Инженер, привыкший к обычным серверам, приходит в Kubernetes и делает то, что делал всегда: экспортирует HTTP_PROXY в окружение, перезапускает процесс и ждёт, что весь исходящий трафик пойдёт через прокси. Иногда это работает. Гораздо чаще это ломает половину кластера, а вторая половина продолжает ходить в интернет напрямую, минуя прокси. И тогда начинается самое интересное: почему одни поды подхватили переменные, а другие нет, почему сервисы перестали видеть друг друга и почему healthcheck вдруг пошёл через прокси-сервер, которого нет в списке доверенных.

Эта статья - подробный разбор того, как на самом деле устроен исходящий трафик в кластере и куда корректно встраивается прокси. Мы не будем пересказывать базовую настройку переменных окружения: это вы и так знаете. Разговор пойдёт о специфике Kubernetes, где привычные подходы дают неожиданные эффекты. Все примеры даны на реальных фрагментах манифестов, а в роли прокси-инфраструктуры выступает Proxeon (proxeon.net).

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

Начнём с фундамента. На классическом сервере понятие исходящего трафика тривиально: есть один сетевой интерфейс, есть таблица маршрутизации, есть системные переменные окружения, и почти всё, что вы запускаете, наследует их от родительской оболочки. Экспортировали переменную в profile - её видит весь пользовательский процесс.

В Kubernetes этой единой точки нет. Здесь есть под - минимальная единица развёртывания, внутри которой живёт один или несколько контейнеров. У пода собственный сетевой namespace, собственный IP-адрес, собственный набор переменных окружения, определяемый манифестом. Оболочки, из которой процесс мог бы что-то унаследовать, попросту нет. Переменная окружения появляется в контейнере только если вы явно объявили её в спецификации пода или в образе.

Что такое исходящий трафик пода

Когда контейнер обращается к внешнему адресу, пакет проходит длинный путь. Сначала он выходит из сетевого namespace контейнера через виртуальный интерфейс. Затем попадает в сетевой стек узла, где им занимается CNI-плагин - компонент, отвечающий за сеть кластера. Далее в дело вступают правила iptables или eBPF, механизм SNAT (подмена исходного адреса), после чего пакет уходит через сетевой интерфейс узла во внешний мир.

Ключевой момент: с точки зрения внешнего сервиса запрос приходит не с адреса пода, а с адреса узла кластера. Под скрыт за трансляцией адресов. Это первое, что удивляет новичков при попытке настроить доступ по белому списку IP: они добавляют в список адрес пода, а трафик приходит с совершенно другого адреса.

Два вида исходящего трафика

Важно с самого начала разделять два принципиально разных потока:

  • Восток-запад - трафик между сервисами внутри кластера. Один под обращается к другому через сервис, ClusterIP или DNS-имя вида my-service.namespace.svc.cluster.local.
  • Север-юг - трафик наружу, к внешним API, базам данных, партнёрским сервисам, объектным хранилищам.

Прокси нужен почти всегда только для трафика север-юг. А самая частая и болезненная ошибка состоит в том, что неправильная настройка прокси случайно захватывает и трафик восток-запад, разрывая внутреннюю коммуникацию. Именно поэтому список исключений здесь важнее, чем где бы то ни было. Но об этом чуть позже.

Глубокое погружение: три уровня, где живёт прокси

Существует ровно три архитектурных уровня, на которых можно встроить прокси в исходящий путь пода. Каждый решает задачу по-своему, у каждого своя цена. Разберём все три честно, с плюсами и минусами.

Уровень 1: сам контейнер

Это когда приложение внутри контейнера само знает про прокси. Оно читает переменные окружения HTTP_PROXY и HTTPS_PROXY либо имеет прокси в собственном конфиге и направляет через него свои HTTP-запросы. Прокси-логика встроена в клиентскую библиотеку самого приложения.

Плюсы. Максимальная простота внедрения на старте. Ничего не нужно добавлять в кластер, никаких дополнительных компонентов. Контроль на уровне конкретного пода: вы точно знаете, какое приложение куда ходит.

Минусы. Каждое приложение обязано уметь читать эти переменные, а это умеют далеко не все. Настройка размазывается по десяткам манифестов. Обновить адрес прокси - значит пройтись по всем деплойментам. Легко забыть про один сервис, и он пойдёт напрямую. Централизованной политики нет.

Уровень 2: sidecar-контейнер

Здесь рядом с основным контейнером в том же поде запускается второй - sidecar. Он перехватывает исходящий трафик и направляет его через прокси. Приложение может вообще не знать о существовании прокси: оно шлёт запросы как обычно, а sidecar незаметно их проксирует. Так работают service mesh вроде Istio и Linkerd, и по этому же принципу можно поставить лёгкий локальный прокси-агент.

Плюсы. Прозрачность для приложения. Единая политика через шаблон пода. Возможность добавить сверх проксирования наблюдаемость: метрики, трейсинг, повторные попытки, тайм-ауты. Sidecar изолирован в поде и делит с ним жизненный цикл.

Минусы. Накладные расходы: на каждый под теперь два контейнера, а значит больше памяти и CPU. Усложняется отладка - в цепочке появляется лишнее звено. Порядок запуска контейнеров важен: если приложение стартует раньше sidecar, первые запросы могут провалиться. В Kubernetes 1.28+ эту проблему решают native sidecar containers как init-контейнеры с политикой restartPolicy Always.

Уровень 3: egress-шлюз кластера

Это выделенный узел или под, через который принудительно направляется весь исходящий трафик кластера. Пакеты маршрутизируются на egress-шлюз средствами CNI, а уже он общается с внешним прокси или сам выступает точкой выхода со стабильным исходящим IP.

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

Минусы. Egress-шлюз становится критической точкой: упал - встал весь север-юг. Нужен резерв и мониторинг. Настройка сложнее и требует поддержки со стороны CNI. Гранулярность ниже: труднее задать разную политику для разных приложений без дополнительных правил.

Как выбрать уровень

Практический ориентир из опыта: маленький проект с парой сервисов, которым нужен внешний прокси, - уровень контейнера. Средний кластер с десятками сервисов и требованием единой политики - sidecar. Крупная инфраструктура, где внешние партнёры требуют фиксированный исходящий IP и аудит всего трафика, - egress-шлюз. Часто уровни комбинируют: egress-шлюз для базового контроля плюс переменные окружения для тонкой настройки отдельных подов через Proxeon.

Переменные окружения в манифесте и роль NO_PROXY

Перейдём к самой недооценённой детали. Переменные HTTP_PROXY, HTTPS_PROXY и NO_PROXY в манифесте задаются через блок env спецификации контейнера. Классические имена принято дублировать в нижнем регистре, потому что часть библиотек читает именно строчные варианты.

apiVersion: apps/v1
kind: Deployment
metadata:
 name: worker
spec:
 replicas: 2
 selector:
matchLabels:
 app: worker
 template:
metadata:
 labels:
app: worker
spec:
 containers:
- name: app
 image: registry.example.com/worker:1.4.0
 env:
- name: HTTP_PROXY
 value: "http://gate.proxeon.net:8080"
- name: HTTPS_PROXY
 value: "http://gate.proxeon.net:8080"
- name: NO_PROXY
 value: "localhost,127.0.0.1,10.0.0.0/8,.svc,.svc.cluster.local,.cluster.local,kubernetes.default"
- name: http_proxy
 value: "http://gate.proxeon.net:8080"
- name: https_proxy
 value: "http://gate.proxeon.net:8080"
- name: no_proxy
 value: "localhost,127.0.0.1,10.0.0.0/8,.svc,.svc.cluster.local,.cluster.local,kubernetes.default"

Почему без правильного NO_PROXY кластер разваливается

Вот суть проблемы. Как только вы объявили HTTP_PROXY, HTTP-клиент приложения начинает направлять через прокси абсолютно все запросы, включая обращения к соседним сервисам внутри кластера. А внутренние сервисы доступны только внутри сети кластера - внешний прокси-сервер до них не достучится физически. Результат: запрос к orders.default.svc.cluster.local уходит на внешний прокси, тот пытается разрезолвить это имя, не может, и возвращает ошибку. Внутренняя коммуникация мгновенно ломается.

NO_PROXY - это список исключений, адреса и домены, которые нужно обходить мимо прокси и слать напрямую. В обычном сервере туда кладут localhost и, может, пару внутренних подсетей. В Kubernetes этот список становится критически важным, потому что он должен покрывать всю внутреннюю топологию кластера. Пропустили один суффикс - и часть межсервисного трафика поехала через внешний прокси.

Что обязательно должно быть в NO_PROXY кластера

  • localhost и 127.0.0.1 - обращения внутри самого пода.
  • Диапазон Pod CIDR - подсеть, из которой выдаются адреса подам, например 10.0.0.0/8 или уточнённый диапазон вашего кластера.
  • Диапазон Service CIDR - подсеть ClusterIP сервисов, часто 10.96.0.0/12.
  • Суффиксы DNS кластера - .svc, .svc.cluster.local, .cluster.local. Именно они покрывают все внутренние DNS-имена сервисов.
  • kubernetes.default - имя API-сервера, к которому обращаются приложения и агенты.
  • Метаданные облака - адрес 169.254.169.254, если вы в облачной среде, чтобы обращения за метаданными не шли через прокси.

Тонкости синтаксиса NO_PROXY

Здесь скрыт целый пласт неочевидностей, на которых спотыкаются даже опытные инженеры.

Во-первых, разные библиотеки по-разному трактуют записи. Одни считают, что .cluster.local с ведущей точкой означает суффикс и совпадёт со всеми поддоменами. Другие требуют формат cluster.local без точки. Практика: указывайте оба варианта, с точкой и без, чтобы покрыть максимум клиентов.

Во-вторых, поддержка CIDR-нотации не универсальна. Библиотека Go понимает 10.0.0.0/8, а вот старые версии некоторых клиентов на других языках - нет, им нужны отдельные адреса или диапазоны в ином формате. Проверяйте поведение именно вашего стека.

В-третьих, порты. Если запись в NO_PROXY указана без порта, обычно она распространяется на любой порт хоста. Но некоторые клиенты сопоставляют строго с портом. При нестандартных портах внутренних сервисов это важно проверить.

Инсайт из практики: девять из десяти инцидентов сломанной внутренней коммуникации после внедрения прокси - это неполный NO_PROXY. Составьте эталонный список для вашего кластера один раз, вынесите его в общий ConfigMap и переиспользуйте во всех деплойментах. Это экономит десятки часов отладки.

Что НЕ подхватывает переменные окружения

Наивное убеждение "я поставил HTTP_PROXY, значит весь трафик пошёл через прокси" в кластере ошибочно вдвойне. Есть целый класс компонентов, которые эти переменные просто игнорируют. Знать их надо заранее.

kubelet и системные компоненты

Переменные окружения контейнера видны только процессам внутри этого контейнера. kubelet - агент узла, который скачивает образы, запускает контейнеры и общается с API-сервером - живёт на уровне узла, а не пода. Он не читает env из манифеста. Если вам нужно, чтобы kubelet тянул образы через прокси, настройка делается на уровне systemd-сервиса kubelet или конфигурации container runtime, а не в спецификации пода.

# /etc/systemd/system/kubelet.service.d/http-proxy.conf
[Service]
Environment="HTTP_PROXY=http://gate.proxeon.net:8080"
Environment="HTTPS_PROXY=http://gate.proxeon.net:8080"
Environment="NO_PROXY=localhost,127.0.0.1,10.0.0.0/8,.svc,.cluster.local"

То же касается container runtime - containerd или CRI-O. Скачивание образов идёт через их собственный сетевой контекст. Настройка прокси для реестра образов задаётся в конфиге runtime, и это отдельная от подов история.

Образы с собственной конфигурацией

Многие популярные образы имеют встроенные настройки сети, которые перекрывают переменные окружения. Например, пакетные менеджеры, веб-серверы или прокси-инструменты внутри образа могут читать собственный конфигурационный файл, а не окружение. Если приложение внутри контейнера использует, скажем, файл настроек с явно прописанными параметрами соединения, ваши env-переменные оно просто не заметит.

Отдельная категория - приложения, где сетевой клиент инициализируется до чтения окружения или кеширует настройки при старте. Изменение переменной без перезапуска процесса тут не даст эффекта.

Отдельные SDK и языковые рантаймы

Это самая коварная группа. Уважение к HTTP_PROXY - соглашение, а не стандарт. Кто-то его соблюдает, кто-то нет.

  • Go. Стандартный http.Client через ProxyFromEnvironment уважает переменные. Но если код создаёт транспорт с явным Proxy nil, переменные игнорируются.
  • Python. Библиотека requests читает окружение по умолчанию. А вот низкоуровневые сокеты, некоторые gRPC-клиенты и асинхронные библиотеки - нет.
  • Java. JVM использует собственные системные свойства http.proxyHost и https.proxyHost, а переменные окружения по умолчанию не читает вовсе. Их нужно передавать через JAVA_TOOL_OPTIONS.
  • Node.js. Встроенный http-модуль переменные окружения не уважает. Нужны сторонние агенты, которые их читают.
  • gRPC. Часть реализаций читает специальную переменную grpc_proxy, а не стандартные.

Вывод простой: нельзя предполагать, что раз переменная объявлена, весь трафик через неё пойдёт. Каждый рантайм проверяйте отдельно. Для JVM, например, манифест выглядит так:

env:
 - name: JAVA_TOOL_OPTIONS
value: "-Dhttp.proxyHost=gate.proxeon.net -Dhttp.proxyPort=8080 -Dhttps.proxyHost=gate.proxeon.net -Dhttps.proxyPort=8080 -Dhttp.nonProxyHosts=localhost|127.0.0.1|*.svc|*.cluster.local"

Обратите внимание: в JVM разделитель исключений - вертикальная черта, а не запятая, и шаблоны используют звёздочку. Ещё одна ловушка для тех, кто механически копирует NO_PROXY.

Секреты: креды прокси через Secret, а не в манифесте

Если ваш прокси требует аутентификации, возникает вопрос хранения логина и пароля. Соблазн велик: вписать их прямо в URL внутри env-значения. Так делать нельзя, и вот почему.

Манифесты деплойментов почти всегда лежат в системе контроля версий. Пароль в открытом виде в env - это утечка кредов в историю Git, доступную всем с доступом к репозиторию. Кроме того, значения env видны любому, кто может выполнить kubectl describe pod. Это прямое нарушение принципа наименьших привилегий.

Правильный путь - объект Secret. Он хранит чувствительные данные отдельно, с возможностью ограничить доступ через RBAC и включить шифрование в хранилище etcd.

apiVersion: v1
kind: Secret
metadata:
 name: proxeon-credentials
type: Opaque
stringData:
 proxy-user: my_account
 proxy-pass: s3cr3t_token_value

Затем подключаем значения из секрета в переменные окружения контейнера через secretKeyRef, а сам URL прокси собираем так, чтобы креды не светились в самом манифесте:

env:
 - name: PROXY_USER
valueFrom:
 secretKeyRef:
name: proxeon-credentials
key: proxy-user
 - name: PROXY_PASS
valueFrom:
 secretKeyRef:
name: proxeon-credentials
key: proxy-pass
 - name: HTTPS_PROXY
value: "http://$(PROXY_USER):$(PROXY_PASS)@gate.proxeon.net:8080"
 - name: NO_PROXY
value: "localhost,127.0.0.1,10.0.0.0/8,.svc,.cluster.local"

Kubernetes подставляет значения из ранее объявленных переменных через синтаксис с долларом и скобками. Это позволяет собрать URL с кредами в рантайме, не помещая пароль в текст манифеста напрямую. Учтите: собранное значение HTTPS_PROXY всё же будет видно в env запущенного контейнера, поэтому дополнительно ограничьте, кто может выполнять exec и describe для этих подов.

Хорошие практики работы с секретами

  • Включите шифрование etcd at rest - иначе секреты хранятся всего лишь в base64, что не является защитой.
  • Ограничьте доступ к секретам через RBAC: под должен видеть только свой секрет.
  • Ротируйте креды прокси регулярно и автоматизируйте перезапуск подов после ротации.
  • Рассмотрите внешние менеджеры секретов с подключением через CSI-драйвер, чтобы креды не попадали в etcd вовсе.
  • Никогда не логируйте собранный URL прокси - пароль утечёт в систему сбора логов.

Sidecar-подход: когда оправдан и что он даёт

Мы уже назвали sidecar одним из трёх уровней. Теперь разберём его как самостоятельную стратегию подробнее, потому что это самый гибкий инструмент, если вы готовы платить за него ресурсами.

Когда sidecar оправдан

  • Приложение не умеет читать HTTP_PROXY и переписать его код нельзя или дорого.
  • Нужна единая политика проксирования без правки каждого приложения.
  • Требуется прозрачный перехват трафика, включая non-HTTP протоколы.
  • Нужна наблюдаемость: метрики исходящих соединений, трейсинг, аудит.
  • Требуются политики повторов, тайм-аутов, разрыва цепи на уровне соединений.

Что даёт sidecar помимо проксирования

Здесь и кроется настоящая ценность. Sidecar - это не просто перенаправитель пакетов. Правильно настроенный, он превращается в точку контроля и наблюдения за всем исходящим трафиком пода.

  • Метрики. Сколько запросов ушло наружу, к каким хостам, с какой задержкой, сколько ошибок. Без sidecar эти данные пришлось бы собирать в каждом приложении отдельно.
  • Трейсинг. Распределённые трейсы исходящих вызовов, привязанные к входящим запросам.
  • Политики надёжности. Автоматические повторы идемпотентных запросов, тайм-ауты, ограничение конкурентных соединений.
  • Единый TLS. Sidecar может терминировать и устанавливать защищённые соединения централизованно.
  • Аудит и соответствие. Полный журнал того, куда именно ходило приложение, - незаменимо для комплаенса.

Пример пода с sidecar-прокси

Ниже упрощённый шаблон, где основной контейнер направляет исходящие HTTP-запросы на локальный sidecar, а тот уже проксирует их дальше через Proxeon. Для приложения адрес прокси - это localhost, что автоматически исключает межсервисный трафик из внешнего проксирования, если приложение обращается к соседям напрямую.

apiVersion: v1
kind: Pod
metadata:
 name: app-with-proxy-sidecar
 labels:
app: billing
spec:
 containers:
- name: app
 image: registry.example.com/billing:2.1.0
 env:
- name: HTTP_PROXY
 value: "http://127.0.0.1:3128"
- name: HTTPS_PROXY
 value: "http://127.0.0.1:3128"
- name: NO_PROXY
 value: "localhost,127.0.0.1,10.0.0.0/8,.svc,.cluster.local"
- name: egress-proxy
 image: registry.example.com/egress-agent:1.0.0
 ports:
- containerPort: 3128
 env:
- name: UPSTREAM_PROXY
 value: "http://gate.proxeon.net:8080"
 resources:
requests:
 cpu: 50m
 memory: 64Mi
limits:
 cpu: 200m
 memory: 128Mi

Native sidecar и порядок запуска

Классическая беда sidecar - гонка при старте. Если основное приложение стартует и делает первый исходящий запрос раньше, чем sidecar готов принимать соединения, запрос падает. Начиная с Kubernetes 1.28 появилась поддержка нативных sidecar-контейнеров: они объявляются в блоке initContainers с restartPolicy Always и гарантированно стартуют и остаются живыми до основного контейнера. Это решает проблему гонки чисто, без костылей вроде задержек и повторов на старте.

spec:
 initContainers:
- name: egress-proxy
 image: registry.example.com/egress-agent:1.0.0
 restartPolicy: Always
 ports:
- containerPort: 3128

Egress-шлюз: стабильный исходящий адрес для кластера

Третий уровень заслуживает отдельного разговора, потому что именно он решает задачу, ради которой чаще всего вообще приходят к прокси в кластере: предсказуемый исходящий IP.

Зачем нужен фиксированный исходящий IP

Внешние партнёры, платёжные шлюзы, поставщики данных нередко работают по белому списку адресов. Они говорят: мы принимаем запросы только с этих IP. В обычном кластере исходящий адрес пода - это адрес узла, на котором под оказался в данный момент. Узлов много, они масштабируются, заменяются, добавляются при автоскейлинге. Адрес непредсказуем. Партнёр не может внести в белый список весь пул узлов, который к тому же меняется.

Egress-шлюз решает это: весь исходящий трафик собирается в одну точку с фиксированным адресом. Партнёр вносит в белый список один-два стабильных IP - и всё работает. При использовании Proxeon в роли исходящей точки вы получаете стабильный внешний адрес, который отдаёте партнёрам один раз.

Как маршрутизируется трафик на egress-шлюз

Механизм зависит от CNI. Некоторые CNI-плагины поддерживают объект egress-политики, где вы описываете: трафик от подов с такими-то метками, идущий наружу, должен выходить через такой-то узел или адрес. Плагин настраивает соответствующие правила SNAT и маршрутизации автоматически. Концептуально это выглядит так:

apiVersion: policy.example.io/v1
kind: EgressPolicy
metadata:
 name: billing-egress
spec:
 selector:
matchLabels:
 egress: proxeon
 egressIP: 203.0.113.10
 destinationCIDRs:
- 0.0.0.0/0

Точный синтаксис отличается между CNI, поэтому сверяйтесь с документацией вашего плагина. Идея неизменна: пометить поды, чей трафик направляется через шлюз, и задать стабильный выходной адрес.

Надёжность egress-шлюза

Раз шлюз - единая точка, он же единая точка отказа. Правила выживания:

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

Диагностика исходящего трафика в кластере

Когда что-то идёт не так - а оно пойдёт, - нужен арсенал проверок. Соберём практический набор команд и подходов.

Шаг 1: узнать реальный исходящий IP из пода

Первое, что проверяем: с какого адреса под виден внешнему миру. Запускаем эфемерный отладочный контейнер или exec в существующий под и обращаемся к сервису, который возвращает ваш публичный адрес.

kubectl run nettest --rm -it --image=registry.example.com/nettools:1.0 --restart=Never -- sh
# внутри контейнера
curl -s https://api.ipify.org
curl -s -x http://gate.proxeon.net:8080 https://api.ipify.org

Первый запрос показывает адрес без прокси, второй - через прокси явно. Если оба одинаковы, а вы ждали разных, значит прокси не применяется. Если второй возвращает ожидаемый адрес Proxeon, прокси работает и дело в конфигурации приложения.

Шаг 2: проверить, видит ли приложение переменные

kubectl exec deploy/worker -c app -- env | grep -i proxy

Если вывод пуст - переменные не долетели до контейнера. Проверяйте манифест и то, что под пересоздан после изменения. Напомню: изменение env требует пересоздания пода, на лету оно не подхватывается.

Шаг 3: проверить резолвинг DNS изнутри

Многие ошибки маскируются под проблему прокси, а на деле это DNS. Проверяем разрешение внутреннего имени и внешнего:

kubectl exec deploy/worker -c app -- nslookup orders.default.svc.cluster.local
kubectl exec deploy/worker -c app -- nslookup api.external-partner.com

Если внутреннее имя не резолвится - проблема в CoreDNS или в том, что запрос ушёл на внешний прокси из-за отсутствия суффикса в NO_PROXY. Это прямое подтверждение, что список исключений неполон.

Шаг 4: где смотреть отказы

  • Логи приложения. Ищите ошибки соединения, тайм-ауты, отказ в аутентификации прокси (обычно код 407).
  • Логи sidecar. Если он есть, там видно, какие запросы он принял и куда направил.
  • Метрики egress-шлюза. Рост отказов и задержек на шлюзе.
  • События пода. kubectl describe pod покажет проблемы запуска, в том числе гонку sidecar.
  • NetworkPolicy. Проверьте, не блокирует ли сетевая политика исходящий трафик - частая причина немых отказов.

Типовые сигнатуры проблем

  • Код 407 от прокси - неверные или отсутствующие креды. Проверяйте Secret.
  • Тайм-аут при обращении к внутреннему сервису после включения прокси - неполный NO_PROXY.
  • Разные исходящие IP при повторных запросах - трафик не идёт через egress-шлюз, работает обычный SNAT узла.
  • Первые запросы после старта падают, потом всё работает - гонка sidecar, переходите на native sidecar.
  • Java-приложение игнорирует прокси - забыли про JAVA_TOOL_OPTIONS и системные свойства.

Типичные ошибки, которых стоит избегать

Соберём в одном месте грабли, на которые наступают чаще всего. Проверьте себя по этому списку до, а не после инцидента.

Неполный NO_PROXY

Абсолютный лидер. Забыли Service CIDR, забыли суффикс .svc, не учли адрес метаданных облака - и получили каскад загадочных отказов. Всегда исходите из полного эталонного списка для вашего кластера.

Пароль прокси в открытом виде в манифесте

Утечка в Git и в вывод describe. Только Secret, только ограниченный доступ.

Предположение, что все рантаймы уважают переменные

JVM, Node.js, часть gRPC-клиентов игнорируют HTTP_PROXY. Проверяйте каждый стек отдельно и настраивайте нативным способом.

Игнорирование kubelet и runtime

Скачивание образов через прокси настраивается на уровне узла, а не пода. Не удивляйтесь, что образы не тянутся, если вы настроили только env в манифесте.

Гонка при старте sidecar

Приложение стартует раньше прокси и роняет первые запросы. Решение - native sidecar containers.

Egress-шлюз без резерва

Единая точка отказа без дублирования кладёт весь исходящий трафик. Резервируйте и мониторьте.

Смешение CIDR-форматов

Указали 10.0.0.0/8 для клиента, который не понимает CIDR. Проверьте, какой формат ест ваша библиотека, и дайте альтернативу.

Отсутствие пересоздания подов после смены env

Изменили переменную в деплойменте, но старые поды продолжают работать со старыми значениями до перезапуска. Обеспечьте прокатку.

Забытый нижний регистр переменных

Часть библиотек читает только http_proxy строчными. Дублируйте имена в обоих регистрах.

Инструменты и ресурсы

Что держать под рукой для работы с исходящим трафиком в кластере.

Диагностические

  • kubectl exec и kubectl debug - основа для проверок изнутри пода.
  • Эфемерные отладочные контейнеры - позволяют подключить набор сетевых инструментов к работающему поду без пересборки образа.
  • Образ с сетевыми утилитами - curl, dig, nslookup, traceroute, собранные в один контейнер для быстрых проверок.
  • Сервис возврата публичного IP - проверка реального исходящего адреса.

Инфраструктурные

  • ConfigMap с эталонным NO_PROXY - единый источник истины для всех деплойментов.
  • Secret и внешние менеджеры секретов - для кредов прокси.
  • Service mesh - если нужен sidecar с наблюдаемостью из коробки.
  • Egress-политики CNI - для маршрутизации на шлюз.
  • Proxeon (proxeon.net) - прокси-инфраструктура для стабильного исходящего адреса и аутентифицированного доступа.

Мониторинг

  • Метрики исходящих соединений с sidecar или egress-шлюза.
  • Синтетические проверки доступности внешних адресов из кластера.
  • Алерты на рост кодов 407 и тайм-аутов исходящих запросов.
  • Дашборд с распределением исходящего трафика по хостам назначения.

Кейсы и результаты

Разберём три обобщённых сценария, показывающих, как выбор уровня влияет на итог.

Кейс 1: платёжная интеграция и белый список IP

Команда интегрировала внешний платёжный шлюз, который принимает запросы только с согласованных адресов. Сначала попробовали переменные окружения на уровне контейнера - и столкнулись с тем, что пр�� автоскейлинге поды разъезжались по новым узлам с новыми адресами, а исходящий IP всё равно оставался адресом узла, не прокси. Часть запросов начала отклоняться.

Решение: перевели платёжный сервис на egress через Proxeon с фиксированным исходящим адресом. Партнёр внёс один IP в белый список. Отказы по причине неизвестного адреса исчезли. Дополнительный эффект - централизованный журнал всех обращений к платёжному шлюзу для аудита.

Кейс 2: разрыв межсервисной коммуникации после внедрения прокси

Компания добавила HTTP_PROXY во все деплойменты одним махом через общий шаблон. Через несколько минут посыпались ошибки: сервисы перестали видеть друг друга. Внутренние вызовы уходили на внешний прокси, который не мог их разрешить.

Диагностика заняла время, пока не проверили резолвинг изнутри пода и не увидели, что имя .svc.cluster.local уходит на прокси. Причина - NO_PROXY содержал только localhost. Составили полный эталонный список с Pod CIDR, Service CIDR и всеми суффиксами DNS, вынесли его в ConfigMap, подключили во все поды. Внутренняя коммуникация восстановилась. С тех пор эталонный NO_PROXY - обязательная часть шаблона деплоймента.

Кейс 3: наблюдаемость исходящего трафика через sidecar

Требование безопасности: знать, куда именно ходит каждое приложение наружу, с полным журналом. Приложения были на разных языках, часть не умела читать HTTP_PROXY. Правка кода десятков сервисов - слишком дорого.

Внедрили sidecar-прокси в шаблон пода. Приложения направляют исходящий трафик на локальный агент, тот проксирует через Proxeon и пишет метрики: хост назначения, задержка, код ответа. Появился дашборд исходящих обращений по каждому сервису. Дополнительно настроили тайм-ауты и повторы на уровне sidecar, что снизило число каскадных сбоев при кратковременной недоступности внешних API. Ценой стали дополнительные ресурсы на sidecar, но выигрыш в наблюдаемости и надёжности это оправдал.

FAQ: частые вопросы инженеров

Почему трафик к соседнему поду идёт через внешний прокси, хотя это очевидно внутренний адрес?

Потому что HTTP-клиент не знает, что адрес внутренний. Он видит имя или адрес и, если тот не входит в NO_PROXY, направляет запрос на прокси согласно правилам. Клиент не различает восток-запад и север-юг сам - за него это делает список исключений. Добавьте внутренние суффиксы и подсети в NO_PROXY.

Нужно ли настраивать прокси и для скачивания образов?

Да, но не через env пода. Скачивание образов выполняет container runtime и kubelet на уровне узла. Прокси для реестра настраивается в конфигурации runtime или в systemd-юните kubelet. Переменные окружения контейнера на это не влияют вообще.

Что важнее для стабильного исходящего IP - sidecar или egress-шлюз?

Egress-шлюз. Sidecar проксирует трафик отдельного пода, но исходящий адрес всё равно определяется тем, куда дальше уходит соединение. Для гарантированно фиксированного адреса, который примет внешний партнёр, нужен либо egress-шлюз, либо выход через внешнюю точку с постоянным адресом, например через Proxeon.

Почему Java-приложение игнорирует HTTP_PROXY?

JVM по историческим причинам не читает переменные окружения для прокси. Она использует собственные системные свойства http.proxyHost, https.proxyHost и исключения http.nonProxyHosts. Передавайте их через JAVA_TOOL_OPTIONS. И помните: разделитель исключений в JVM - вертикальная черта, а шаблоны со звёздочкой, а не CIDR.

Как безопасно хранить пароль прокси?

В объекте Secret с ограничением доступа по RBAC и включённым шифрованием etcd. Подключайте значения через secretKeyRef. Не пишите пароль в открытом виде в манифест, иначе он утечёт в Git и в вывод describe. Для повышенных требований используйте внешний менеджер секретов через CSI.

Изменил переменную окружения, а поведение не поменялось - почему?

Переменные окружения фиксируются при старте контейнера. Пока под не пересоздан, он работает со старыми значениями. Обновите деплоймент так, чтобы прошла прокатка подов. Также проверьте, что приложение вообще читает окружение, а не кеширует настройки при инициализации.

Как понять, какой формат NO_PROXY нужен именно моей библиотеке?

Эмпирически: настройте прокси, обратитесь к внутреннему имени изнутри пода и посмотрите, ушёл ли запрос на прокси или напрямую. Если ушёл на прокси - формат не распознан. Пробуйте вариант с ведущей точкой и без, добавляйте отдельные адреса вместо CIDR, проверяйте регистр имени переменной. Документация конкретного клиента - лучший ориентир.

Стоит ли всегда использовать service mesh ради проксирования?

Нет. Service mesh - мощный инструмент с наблюдаемостью и политиками, но он несёт заметные накладные расходы и операционную сложность. Если задача - только направить исходящий трафик через прокси, лёгкий sidecar-агент или egress-шлюз обойдутся дешевле. Внедряйте mesh, когда вам действительно нужны его возможности целиком.

Как быть с трафиком, который вообще не HTTP?

Переменные HTTP_PROXY работают только для HTTP и HTTPS-клиентов, которые их уважают. Для произвольных TCP-соединений нужен прозрачный перехват на уровне sidecar или egress-шлюза с соответствующими правилами маршрутизации. Здесь переменные окружения бессильны.

Можно ли задать разную политику прокси для разных подов?

Да. На уровне контейнера - через разные env в разных деплойментах. На уровне egress - через селекторы меток в egress-политиках, направляя помеченные поды на нужный выход. Комбинирование даёт гибкость: базовый выход через шлюз плюс индивидуальные настройки для отдельных сервисов.

Заключение: собираем чеклист выката

Мы прошли путь от того, почему настройка "как на обычном сервере" не работает в кластере, до трёх уровней встраивания прокси, тонкостей NO_PROXY, секретов, sidecar, egress-шлюза и диагностики. Главный вывод: в Kubernetes исходящий трафик - это не одна переменная, а слоёная система, где важнее всего не то, как включить прокси, а то, как не сломать им внутреннюю коммуникацию.

Соберём финальный чеклист выката, который стоит держать перед глазами при каждом внедрении.