Вас ждет захватывающая история того, как несчастный девопс копался в исходниках всего, что подвернется под руку, просто чтобы заработала одна команда. В посте прячутся костыли, добро, и целая масса ковролина. Чтобы фантасмагория была понятнее, поясню: все события происходили на macOS (darwin/amd64).
Введение (издалека)
Мне нравится OpenStack. Хорошая система, настоящая операционная система для
датацентра (куберы рядом с ним ничего не стоят). Как и положено настоящей
операционной системе, тем более для вычислительного кластера, она состоит из
большого числа компонентов, и постижение ее требует свитера и бороды. Один из ее
компонентов - OpenStack Magnum, штука,
создающая кластеры k8s одной командой. Причем, как и положено куберам, которые
предоставляются облачным провайдером (OpenStack - это облако, просто добавь
своей воды), кластер не простой, а с интеграциями. Хранилище с помощью Cinder,
балансировщики нагрузки с помощью Neutron, и, что самое важное для нашего
рассказа - авторизация с помощью Keystone, то есть управлять доступом
пользователей в кластер можно родным функционалом OpenStack. Чтобы это работало
на клиенте, нужен соответствующая утилита командной строки -
client-keystone-auth.
Я охотно настроил это в своих кластерах, затюнил свой ~/.kube/config
, и вместо
того, чтобы все обладали админскими правами, пользуясь одним общим
kubeconfig1, каждый радостно ходит под своими кредами. Был бы еще не
Keystone, а Active Directory (мы же ынтерпраз), вообще была бы сказка. Так как
наши кластеры работают по принципу “настроил и забыл”, я не пользовался этим
несколько месяцев.
Малец подкрался незаметно
В силу <текущих событий>, работа происходит из дома, по корпоративному VPN. При
работе из дома случилась странная вещь - та хваленая авторизация в кластер через
Keystone перестала работать, выкидывая ошибку dial tcp: i/o timeout
.
Соединение не могло установиться, хотя старый добрый cURL проблем с подключением
не видел. Странно, но так как все деплои были настроены через CI/CD, и ничего не
падало, желания что-то с этим делать особо не было. Наверное, у go был какой-то
баг с работой во внутренней сети. Достали пыльные админские kubeconfig, и
продолжаем работу.
Сенпай не мог не заметить
Еще один день, еще одна настройка личного кластера в Docker For Mac2. Надо настроить Nginx Ingress Controller. Для установки (настраивать, в общем-то, ничего не надо) есть простая команда:
$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v0.35.0/deploy/static/provider/cloud/deploy.yaml
Unable to connect to the server: dial tcp: i/o timeout
Как говорит Никита Инстаграмщик, опачки. Это уже переходит границы разумного.
Сервера гитхаба - публичная сеть, как дотуда-то не достучаться? Пришло время
надеть костюм ассенизатора3 и погрузиться в исходники. Открываем репо
kubectl4, и начинаем внимательно
изучать. Естественно, залезаем в
pkg/cmd/apply/apply.go
,
внимательно пролистываем исходники и распутываем стек. Оказываемся мы
совершенно в другом
репозитории,
на строчке resp, err := http.Get(url)
. То есть в итоге дергается стандартный
метод стандартной библиотеки без каких-либо прикрас.
Воспроизведем проблему?
Как известно, без веских оснований поносить стандартную библиотеку языка, да и любой другой софт - моветон. Надо понять, что вызывает это. Что нам стоит, проект на го построить. Да какой!
$ cat main.go
package main
import (
"net/http"
)
func main() {
conn, err := http.Get("https://raw.githubusercontent.com")
if err != nil {
panic(err)
}
conn.Close()
}
$ go run main.go || echo failed
$
Все работает, почему у них не так? Пришло время для тяжелого вооружения - слушать системные вызовы. Ах да, это же macOS, и на ней что-то потрассировать та еще боль. Ну, как известно, искать лучше где светло. Берем lsof и смотрим, где же утилита зависает:
$ lsof -p 31464
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
kubectl 31464 anton cwd DIR 1,5 416 4010040 /Users/anton/Documents/dev/
kubectl 31464 anton txt REG 1,5 49469232 9837065 /Applications/Docker.app/Contents/Resources/bin/kubectl
kubectl 31464 anton txt REG 1,5 1558736 1152921500311885591 /usr/lib/dyld
kubectl 31464 anton 0u CHR 16,5 0t14303394 2783 /dev/ttys005
kubectl 31464 anton 1u CHR 16,5 0t14303394 2783 /dev/ttys005
kubectl 31464 anton 2u CHR 16,5 0t14303394 2783 /dev/ttys005
kubectl 31464 anton 3 PIPE 0x60cd7d7cef790dc0 16384 ->0xe37ce7275a1d221a
kubectl 31464 anton 4 PIPE 0xe37ce7275a1d221a 16384 ->0x60cd7d7cef790dc0
kubectl 31464 anton 5u IPv4 0xe39ef2152b07d37f 0t0 TCP localhost:52420->localhost:sun-sr-https (ESTABLISHED)
kubectl 31464 anton 6u KQUEUE count=0, state=0xa
kubectl 31464 anton 7r CHR 14,1 0t4096 586 /dev/urandom
kubectl 31464 anton 8u IPv4 0xe39ef214ee781477 0t0 UDP 192.168.1.49:52793->ns3.corbina.net:domain
kubectl 31464 anton 9u IPv4 0xe39ef214ee77fa4f 0t0 UDP 192.168.1.49:57961->ns3.corbina.net:domain
Interessant. Умирает на UDP запросах к провайдеру. DNS умер? Но операционная система работает исправно, сеть есть, сайты открываются. Что же это может быть? Ручной nslookup так же отказывается работать с публичными DNS серверами. VPN? Открываем таблицу роутинга и внимательно высматриваем, что же пошло не так. Нет, IP-адреса DNS не роутятся в интранет. Может, у Go какой-то хитрый резолвер? Да нет, ведь свой доброскрипт работает…
На каждый хитрый ларчик найдется своя монтировка
Golang имеет два способа резолвить доменные имена:
- Резолвер здорового человека: getaddrinfo(3), getnameinfo(3). Требует отдельный тред для резолва.
- Резолвер курильщика, который решил, что тратить целый тред плохо, и заимплементил свой резолвер на чистом го. Молодец! Кажется, я нашел, где бутылочное горлышко.
Но ладно, полили дизайнеров Go грязью5, надо добавить ложку меда: резолвер
настраивается через переменную окружения прямо в рантайме! GODEBUG=netdns=cgo
лечит почти любой вид рака и работает в сложных условиях VPN. И действительно:
$ GODEBUG=netdns=go go run main.go || echo failed
Unable to connect to the server: dial tcp: i/o timeout
failed
$
$ GODEBUG=netdns=cgo go run main.go || echo failed
$
Ну что, теперь, наверное, можно просто выставить эту переменную окружения kubectl, и все заработает?
$ GODEBUG=netdns=cgo kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v0.35.0/deploy/static/provider/cloud/deploy.yaml
Unable to connect to the server: dial tcp: i/o timeout
Отчаяние
Стоковые kubectl, keystone-auth-client, и многие другие тулы, доступные на мак,
собираются с флагом CGO_ENABLED=false
, зачем им зря рантайм засорять?
Что делать, теперь у меня есть любовно собранные своими руками утилитки на го, с
вкоряченными в произвольные места флагами компиляции. Я буду дома вовремя!
Погоди, а что же не так с VPN?
Cisco AnyConnect - очень хитрый VPN. Все DNS-запросы, сделанные системными вызовами, перехватываются и направляются куда надо. Все случайные UDP-запросы на 53 порт просто дропаются. Отсюда попытка написать свой резолвер обречена на неудачу. Почему они заодно не изменяют системный список DNS сервера остается загадкой. Можно сделать это за них, и прописать у себя внутренние DNS. Если компьютер рабочий, это не так страшно. Если же компьютер свой, то не забывайте: Иисус страдал, и тебе завещал.
-
Magnum умеет подписывать админские сертификаты для доступа в кластер. Позволяет логиниться в любых условиях, насколько бы сильно ни был сломан вебхук для аутентикации/авторизации. ↩︎
-
Черт возьми, куберы, встроенные в Docker For Mac бесподобны. Ни разу не возникло проблемы, что чарт не вставал, или не хотел что-то делать. Совершенство из коробки. ↩︎
-
Кого я обманываю, я этот костюм и не снимаю. ↩︎
-
Ридми не читай @ умом страдай. Этот репозиторий содержит большинство кода kubectl, но не сам клиент. Кто ж знал? ↩︎
-
У всех свои хобби. ↩︎