Поды с сайдкаром OpenVPN сбоят в разрешении имён по узнаваемому образцу: имена кластера зависают, удалённые имена отвечают до перезапуска пода, а отредактированный файл резолвера возвращается. Ничто в этой последовательности не является неисправностью туннеля. У пода один файл резолвера и два мира имён, и файл может перечислить серверы по порядку, но не может выразить зоны. Устойчивое решение — резолвер, работающий внутри пода и владеющий разделением, поэтому файл остаётся простым и ни одна политика не имеет повода его переписывать.

Почему сделанное изменение всё время исчезает

Файл принадлежит kubelet, а не вам. В рамках dnsPolicy: ClusterFirst kubelet создаёт его, чтобы указывать на DNS кластера, и пересоздаёт при каждом старте контейнера. Всё, что вы меняете через kubectl exec, живёт в записываемом слое контейнера, поэтому перезапуск, вытеснение или rollout это выбрасывают. Спецификация пода по-прежнему несёт ту же политику, поэтому возвращается тот же файл.

Установка dnsPolicy: None останавливает пересоздание и убирает разрешение имён кластера. Вы получаете файл под своим контролем и теряете нужные имена. Этот обмен и есть вся проблема: resolv.conf не знает понятия зон, только серверы, пробуемые по порядку, поэтому не может отправить cluster.local на один адрес, а приватную зону на другой.

Запустите один резолвер в поде, на loopback

Направьте под на единственный сервер имён на loopback, затем позвольте контейнеру CoreDNS, разделяющему сетевое пространство имён пода, отвечать на порту 53. Задайте политику None, дайте поду этот единственный адрес и добавьте контейнер DNS рядом с VPN и приложением:

spec:
  dnsPolicy: None
  dnsConfig:
    nameservers:
      - 127.0.0.1
    options:
      - name: ndots
        value: "5"
  containers:
    - name: dns
      image: coredns/coredns
      args: ["-conf", "/etc/coredns/Corefile"]
      volumeMounts:
        - {name: corefile, mountPath: /etc/coredns, readOnly: true}
    - name: vpn
      image: your-openvpn-client-image
    - name: app
      image: your-app-image
  volumes:
    - name: corefile
      configMap:
        name: split-dns-corefile

Все три контейнера разделяют одно сетевое пространство имён, поэтому 127.0.0.1:53 из контейнера приложения достигает CoreDNS без сервиса, без port-forward и без обхода через сеть кластера. Монтирование Corefile из ConfigMap сохраняет его как версионируемый вход, а не файл, редактируемый на месте. Сгенерированный resolv.conf тогда содержит один сервер имён и ничего больше — состояние, о котором можно рассуждать.

Здесь кусаются две детали. Официальный образ привязывает порт 53 как root, и некоторые политики кластера требуют написать это явно. Каждый контейнер пода теперь зависит от работы того единственного контейнера, поэтому добавьте readiness-пробу на безобидное имя и позвольте приложению терпеть ошибки старта.

Дайте каждой приватной зоне свой upstream

Corefile — это список зон, каждая называет сервер, который может на неё ответить. Один блок покрывает зону туннеля, а блок catch-all идёт последним и забирает всё, на что не претендовал ни один явный блок. Catch-all должен указывать на настоящий upstream, никогда обратно в под: .:53 { forward . 10.100.0.10 } повторно использует резолвер кластера, потому что тот уже пересылает публичные имена резолверам самого узла. Ничему другому в поде не нужно знать, к какой сети относится имя.

.:53 {
  errors
  cache 30
  forward . 10.100.0.10
}

svc.acme.internal:53 {
  errors
  cache 30
  forward . 192.168.240.1
}

Читайте адрес кластера из сервиса kube-dns в kube-system, а не копируйте из статьи, потому что CIDR сервисов различаются. Адрес туннеля приходит из значения dhcp-option DNS, которое отправляет сервер OpenVPN, или от авторитетного сервера этой зоны. Если ваш резолвер кластера не отвечает на публичные имена, поместите адрес резолвера узла в блок catch-all, прочитанный на узле, а не внутри пода.

Не направляйте catch-all на /etc/resolv.conf — это соблазнительная однострочная настройка и ловушка именно в этой конфигурации. В рамках dnsPolicy: None файл самого пода содержит единственный сервер имён 127.0.0.1, поэтому эта цель — сам CoreDNS, и каждый несовпавший запрос повторяется до истечения времени. Блок с именем svc.acme.internal совпадает с этим именем и каждым именем под ним, что покрывает db.svc.acme.internal и исключает svc.acme.internal.example.net. Точка в конце косметична, потому что имена зон нормализуются до полной формы; граница суффикса решает, куда попадёт запрос.

Решите, принадлежит ли пересылка резолверу кластера

Если каждой рабочей нагрузке в пространстве имён нужна зона туннеля, блок пересылки в ConfigMap CoreDNS под kube-system дешевле сайдкара на каждый под. Это изменение уровня кластера: нужны права администратора и перезапуск развёртывания, от которого зависят все. Ограничьте его той единственной зоной, никогда корнем. Адрес туннеля одной нагрузки становится тогда резолвером каждого пода для зоны, и в день падения туннеля весь кластер теряет имя.

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

Имена, которые разрешаются, пока соединения ещё падают

Разрешение — одна из двух вещей, которые туннель должен нести; вторая — обратный путь, и сеть пода хорошо его прячет. Если сам сервер OpenVPN работает как под, правила, добавленные в том контейнере, не управляют трафиком, уходящим через узел. Этот сбой узок: ответы получали только поды на узле, который тоже запускал сервер, а всё остальное истекло.

Запуск сервера с hostNetwork: true возвращает его в пространство имён узла, где действуют правило FORWARD и трансляция источника. Транслируйте пул клиентов на выходе, иначе ни один удалённый узел не имеет маршрута назад к адресу туннеля вроде 10.8.0.6. Проверьте оба направления, прежде чем снова трогать Corefile. Туннель, который несёт запрос, но не ответ, даёт ровно тот же таймаут, что и резолвер, никогда не получивший запрос.

Проверяйте из нового пода, а не из сеанса, который вы починили

Спросите обе зоны явно, затем повторите запрос после замены пода. Конфигурация, существующая только в работающем контейнере, не является конфигурацией:

kubectl exec -it deploy/app -- dig @127.0.0.1 kubernetes.default.svc.cluster.local +short
kubectl exec -it deploy/app -- dig @127.0.0.1 db.svc.acme.internal +short
kubectl delete pod -l app=your-app
kubectl wait --for=condition=Ready pod -l app=your-app --timeout=90s
kubectl exec -it deploy/app -- dig @127.0.0.1 db.svc.acme.internal +short

Если первая пара отвечает, а пара после замены нет, Corefile в порядке, а спецификация пода нет. Если обе отвечают, а приложение всё ещё не подключается, прекратите отладку DNS и проверьте путь TCP через nc -vz db.svc.acme.internal 5432 изнутри контейнера. Сохраните вывод dig в тикете, потому что это артефакт, отделяющий проблему имён от проблемы маршрутизации.

Похожие статьи