Эта статья написана потому, что я бы хотел иметь такую статью
перед глазами, когда развертывал кластер по документации. Сразу
хочу сказать, что не являюсь экспертом в K8S, однако имел опыт с
развертыванием продуктовых установок DC/OS (экосистемы, основанной
на Apache Mesos). Долгое время K8S меня отпугивал тем, что, при
попытке его изучения, тебя закидывают кучей концепций и терминов,
отчего мозг взрывается.
Тем не менее, у меня возникла задача настроить отказоустойчивый
Bare Metal кластер для комплексного приложения, в связи с чем и
возникла данная статья. В процессе руководства я затрону следующие
аспекты:
-
корректная установка с помощью kubeadm на узлах с несколькими
NIC;
-
реализация отказоустойчивого Control Plane с доступом по общему
IP и DNS-имени;
-
реализация Ingress контроллера на базе Nginx на выделенных узлах
с доступом из публичной сети;
-
проброс K8S API в публичную сеть;
-
проброс K8S Dashboard UI в публичную сеть.
Я выполнял установку в среде Ubuntu 18.04, в связи с чем часть
из шагов может не работать в вашем дистрибутиве. В тексте могут
встречаться фразы "как я понял..." и "я не до сих пор не вполне
понял...".
Сначала рассмотрим узловую топологию кластера, в котором мы
будем развертывать K8S. Это упрощенная принципиальная топология,
чтобы не загромождать публикацию лишними деталями.
Отличительной особенностью в моем кластере является то, что у
всех узлов имеется два сетевых интерфейся - в моем случае, на eth0
всегда находится публичный адрес, а на eth1
- адрес из
сети 10.120.0.0/16
.
Стоит отметить, что K8S исключительно проще настраивать в
случае, если ваши машины имеют по одному NIC. Если ваша
инфраструктура позволяет вам использовать машины с одним сетевым
устройством и иметь внешний балансировщик нагрузки - однозначно так
необходимо делать.
В случае моей же инфраструктуры такая топология хотя и возможна,
но я понял насколько K8S становится проще при использовании машин с
одной NIC уже после того, как начал развертывание, поэтому решил
"победить" проблемы, в рамках данной топологии, сохранив
преимущества доступа к каждой машине напрямую.
Еще, kubeadm при развертывании считает, что что правильный
IP-адрес - это тот, который находится в сети, где шлюз
"по-умолчанию", если не переопределить, ничего хорошего не
выйдет.
Тем не менее, в случае использования Enterprise решений, машины
с несколькими сетевыми картами и доступом к ним из разных сетей -
достаточно рядовая история, поэтому данная модель развертывания
имеет право на существование.
Хочу отметить, что я использую Ansible, но в качестве упрощения
не буду в статье демонстрировать playbook-и, ориентируясь на
настройку руками. Итак, приступим.
Замена DNS-рекурсора
Я хочу обеспечить доступность всех узлов кластера через
DNS-имена по внутренним IP-адресам, при этом сохранив доступность
разрешения обычных имен узлов в интернете. Для этого, на серверах
gw-1
, gw-2
я разверну pdns-recursor и
укажу его в качестве рекурсора на всех узлах кластера.
Базовая настройка pdns-recursor на gw-1
,
gw-2
включает указание следующих директив:
allow-from=10.120.0.0/8, 127.0.0.0/8etc-hosts-file=/etc/hosts.resolvexport-etc-hosts=onexport-etc-hosts-search-suffix=cluster
Сам файл /etc/hosts.resolv
генерируется с помощью
ansible и выглядит следующим образом:
# Ansible managed10.120.29.231 gw-1 gw-110.120.28.23 gw-2 gw-210.120.29.32 video-accessors-1 video-accessors-110.120.29.226 video-accessors-2 video-accessors-210.120.29.153 mongo-1 mongo-110.120.29.210 mongo-2 mongo-210.120.29.220 mongo-3 mongo-310.120.28.172 compute-1 compute-110.120.28.26 compute-2 compute-210.120.29.70 compute-3 compute-310.120.28.127 zk-1 zk-110.120.29.110 zk-2 zk-210.120.29.245 zk-3 zk-310.120.28.21 minio-1 minio-110.120.28.25 minio-2 minio-210.120.28.158 minio-3 minio-310.120.28.122 minio-4 minio-410.120.29.187 k8s-1 k8s-110.120.28.37 k8s-2 k8s-210.120.29.204 k8s-3 k8s-310.120.29.135 kafka-1 kafka-110.120.29.144 kafka-2 kafka-210.120.28.130 kafka-3 kafka-310.120.29.194 clickhouse-1 clickhouse-110.120.28.66 clickhouse-2 clickhouse-210.120.28.61 clickhouse-3 clickhouse-310.120.29.244 app-1 app-110.120.29.228 app-2 app-210.120.29.33 prometeus prometeus10.120.29.222 manager manager10.120.29.187 k8s-cp
Шаблон Ansible для генерации конфига
# {{ ansible_managed }}{% for item in groups['all'] %}{% set short_name = item.split('.') %}{{ hostvars[item]['host'] }} {{ item }} {{ short_name[0] }}{% endfor %}10.120.0.1 k8s-cp
Теперь, необходимо сделать так, чтобы все узлы вместо
DNS-рекурсоров, получаемых из настроек DHCP, использовали данные
DNS-ы. В Ubuntu 18.04 используется systemd-resolved
,
поэтому необходимо ему указать требуемые серверы gw-1,
gw-2
. Для этого определим манифест, переопределяющий
поведение systemd-resolved с помощью файла
/etc/systemd/network/0-eth0.network
на каждом хосте
кластера:
[Match]Name=eth0[Network]DHCP=ipv4DNS=10.120.28.23 10.120.29.231Domains=cluster[DHCP]UseDNS=falseUseDomains=false
Делает он следующее, для DHCP-записи, полученной через eth0
будут игнорироваться DNS-серверы и поисковые домены. Вместо этого
будут использоваться серверы 10.120.28.23,
10.120.29.231
и использоваться поисковый домен
*.cluster
. После создания данного файла требуется
перезагрузить узел или сеть узла, поскольку простой перезапуск
systemd-resolved
не инициирует повторное получение
данных по DHCP. Я перезагружаю для того, чтобы убедиться в
корректном поведении при старте узла.
При успешной инициализации systemd-resolve --status
выдаст следующий листинг:
Global DNSSEC NTA: 10.in-addr.arpa 16.172.in-addr.arpa 168.192.in-addr.arpa 17.172.in-addr.arpa 18.172.in-addr.arpa 19.172.in-addr.arpa 20.172.in-addr.arpa 21.172.in-addr.arpa 22.172.in-addr.arpa 23.172.in-addr.arpa 24.172.in-addr.arpa 25.172.in-addr.arpa 26.172.in-addr.arpa 27.172.in-addr.arpa 28.172.in-addr.arpa 29.172.in-addr.arpa 30.172.in-addr.arpa 31.172.in-addr.arpa corp d.f.ip6.arpa home internal intranet lan local private testLink 3 (eth1) Current Scopes: none LLMNR setting: yesMulticastDNS setting: no DNSSEC setting: no DNSSEC supported: noLink 2 (eth0) Current Scopes: DNS LLMNR setting: yesMulticastDNS setting: no DNSSEC setting: no DNSSEC supported: no DNS Servers: 10.120.28.23 10.120.29.231 DNS Domain: cluster
Это действие необходимо выполнить на всех узлах кластера. При
корректном выполнении каждый узел сможет выполнить ping
gw-1.cluster
, ping gw-2.cluster
и получить
ответ от данных узлов по внутренним ip-адресам.
Отключение раздела подкачки
Выполняется на всех узлах. Kubernetes не хочет
работать при наличии разделов подкачки на узлах. Для их отключения
вы можете воспользоваться следующим скриптом:
sudo -- sh -c "swapoff -a && sed -i '/ swap / s/^/#/' /etc/fstab"
Для пущей уверенности удалите swap-раздел с помощью
fdisk
.
Внесение изменений в сетевые настройки ядра
Выполняется на всех узлах. Я буду использовать
Flannel - простейший оверлейный сетевой провайдер для K8S. Обратите
внимание, что довольно много провайдеров используют
VXLAN
. Это накладывает определенные особенности для
сетевой инфраструктуры.
В простейшем случае, вам требуется обеспечить работоспособность
multicast
, поскольку VXLAN
использует
multicast
-группы, для своей работы. Если
multicast
- не ваш вариант, но вы хотите использовать
провайдер, основанный на VXLAN
, можно настроить работу
VXLAN
через BGP или другими способами. Однако,
сможет ли жить с этим выбранный вами провайдер сетевой
инфраструктуры Kubernetes - это большой вопрос. В общем, Flannel
поддерживает VXLAN
через multicast. В моем случае это
VXLAN+multicast
over VXLAN+multicast
over
Ethernet
, поскольку в моей сети виртуальные машины
имеют VXLAN-бэкбон
, работающий поверх
Ethernet
с использованием multicast
- так
тоже работает.
В /etc/modules добавьте br_netfilter,
overlay
.
Выполните modprobe br_netfilter && modprobe
overlay
, чтобы загрузить модули.
В /etc/sysctl.conf
добавьте:
net.bridge.bridge-nf-call-ip6tables = 1net.bridge.bridge-nf-call-iptables = 1net.ipv4.ip_forward = 1
Выполните sysctl -p
для применения изменений.
Установка containerd
Выполняется на всех узлах. Kubernetes
рекомендует использовать containerd (впрочем, новые версии docker
тоже используют containerd), поэтому установим его:
sudo apt-get updatesudo apt install containerdsudo sh -- -c "containerd config default | tee /etc/containerd/config.toml"sudo service containerd restart
Установим kubeadm, kubelet, kubectl
Выполняется на всех узлах. Здесь прям из
руководства по установке K8S:
sudo apt-get update && sudo apt-get install -y apt-transport-https curlcurl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -cat <<EOF | sudo tee /etc/apt/sources.list.d/kubernetes.listdeb https://apt.kubernetes.io/ kubernetes-xenial mainEOFsudo apt-get updatesudo apt-get install -y kubelet kubeadm kubectlsudo apt-mark hold kubelet kubeadm kubectl
Инициализация первого узла K8S
Выполняется на узлах, которые будут обслуживать Control Plane
K8S - в моем случае k8s-{1,2,3}
. Здесь уже есть
нюансы, специфичные для моего развертывания:
kubeadm init --pod-network-cidr=10.244.0.0/16 \ --control-plane-endpoint=k8s-cp \ --apiserver-advertise-address=10.120.29.187
Для Flannel категорически важно использовать
--pod-network-cidr=10.244.0.0/16
. С другим адресным
пространством для POD-ов K8S он не запустится.
--control-plane-endpoint string Specify a stable IP address or
DNS name for the control plane.
Здесь Вы должны указать тот DNS или IP, который будет
использоваться для связи всех шурушков K8S-а с Control Plane. Я
решил использовать доменное имя k8s-cp
, привязанное к
отказоустойчивому ip-адресу 10.120.0.1
(см. далее, на
текущий момент, k8s-cp указывает на один из серверов Control Plane:
10.120.29.187 k8s-cp
).
Важный аргумент --api-server-advertise-address
.
Важно, что он влияет не только на api-server
, но и на
Etcd
, что нигде не сказано, но очень важно для
отказоустойчивой топологии. Если ничего не указать, то
kubeadm
возьмет адрес с той сети, в которой шлюз
по-умолчанию, что не всегда верно. В моем случае это приводит к
тому, что Etcd
стартует на публичном интерфейсе, а
кластер Etcd
хочет работать по публичной сети, что
меня не устраивает. Если этот адрес не указать правильно, то
Flannel тоже не сможет корректно инициализироваться, будет падать с
ошибками, что не может связаться с Control Plane (будет
использовать тот же IP-адрес из сети со шлюзом по умолчанию для
связи).
В общем, этот параметр привносит много геморроя и ведет к
некорректной работе всего, что только может некорректно
работать.
Теперь, если все хорошо, то Kubeadm
развернет K8S
на данном узле. Я рекомендую выполнить перезагрузку узла для того,
чтобы убедиться что Control Plane стартует как надо. Убедитесь в
этом, запросив список выполняемых задач:
ps xa | grep -E '(kube-apiserver|etcd|kube-proxy|kube-controller-manager|kube-scheduler)'
Теперь можно скопmkdir -p $HOME/.kube sudo cp -i
/etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id
-u):$(id -g) $HOME/.kube/config
ировать настройки
конфигурации для доступа администратора к кластеру в домашний
каталог:
mkdir -p $HOME/.kubesudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/configsudo chown $(id -u):$(id -g) $HOME/.kube/config
Для проверки запросите задачи, выполняемые на данном настроенном
контроллере посредством команды kubectl get pods
--all-namespaces
. Вы должны получить вывод, примерно
соответствующий следующему:
NAMESPACE NAME READY STATUS RESTARTS AGEkube-system etcd-k8s-1 1/1 Running 0 2d23hkube-system kube-apiserver-k8s-1 1/1 Running 0 2d23hkube-system kube-controller-manager-k8s-1 1/1 Running 1 2d23hkube-system kube-scheduler-k8s-1 1/1 Running 1 2d23h
Я рекомендую посмотреть вывод этой команды еще пару раз, с
перерывом 1 минуту, чтобы убедиться, что RESTARTS
не
растут, а статус Running
.
Сам Kubernetes никакой сети не предоставляет, делегируя это
плагинам CNI. Мы будем
использовать простой CNI - Flannel. Его установка производится
элементарно следующей командой:
kubectl apply -f https://github.com/coreos/flannel/raw/master/Documentation/kube-flannel.yml
Опять же, выполните kubectl get pods
--all-namespaces
несколько раз, чтобы убедиться, что Flannel
выполняется без ошибок и RESTARTS не растут. Если что-то пошло не
так, посмотрите журнал событий flannel следующим способом (только
используйте настоящее имя POD-а Flannel:
kubectl logs -n kube-system kube-flannel-ds-xn2j9
Теперь вы можете подключить два других узла Control Plane с
помощью команды, выполненной на каждом из них:
# ssh k8s-2sudo kubeadm join k8s-cp:6443 --apiserver-advertise-address=10.120.28.37 --token tfqsms.kiek2vk129tpf0b7 --discovery-token-ca-cert-hash sha256:0c446bfabcd99aae7e650d110f8b9d6058cac432078c4fXXXXX6055b4bd --control-planemkdir -p $HOME/.kubesudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/configsudo chown $(id -u):$(id -g) $HOME/.kube/config# ssh k8s-3sudo kubeadm join k8s-cp:6443 --apiserver-advertise-address=10.120.29.204 --token tfqsms.kiek2vk129tpf0b7 --discovery-token-ca-cert-hash sha256:0c446bfabcd99aae7e650d110f8b9d6058cac432078c4fXXXXXec6055b4bd --control-planemkdir -p $HOME/.kubesudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/configsudo chown $(id -u):$(id -g) $HOME/.kube/config
Если вдруг --token
"протух", используйте команду
kubeadm token create
, чтобы сгенерировать новый.
После подключения узла смотрите внимательно за состоянием
Etcd:
kubectl get pods --all-namespaces | grep etcdkube-system etcd-k8s-1 1/1 Running 0 2d23hkube-system etcd-k8s-2 1/1 Running 1 2d22hkube-system etcd-k8s-3 1/1 Running 1 2d22h
Состояние должно быть Running, увеличение счетчика перезагрузок
не должно происходить. Команда kubectl get pods --all-namespaces
должна отображать трехкратный набор процессов на всех узлах Control
Plane:
NAME READY STATUS RESTARTS AGEcoredns-74ff55c5b-h2zjq 1/1 Running 0 2d23hcoredns-74ff55c5b-n6b49 1/1 Running 0 2d23hetcd-k8s-1 1/1 Running 0 2d23hetcd-k8s-2 1/1 Running 1 2d22hetcd-k8s-3 1/1 Running 1 2d22hkube-apiserver-k8s-1 1/1 Running 0 2d23hkube-apiserver-k8s-2 1/1 Running 1 2d22hkube-apiserver-k8s-3 1/1 Running 1 2d22hkube-controller-manager-k8s-1 1/1 Running 1 2d23hkube-controller-manager-k8s-2 1/1 Running 1 2d22hkube-controller-manager-k8s-3 1/1 Running 1 2d22hkube-flannel-ds-2f6d5 1/1 Running 0 2d3hkube-flannel-ds-2p5vx 1/1 Running 0 2d3hkube-flannel-ds-4ng99 1/1 Running 3 2d22hkube-proxy-22jpt 1/1 Running 0 2d3hkube-proxy-25rxn 1/1 Running 0 2d23hkube-proxy-2qp8r 1/1 Running 0 2d3hkube-scheduler-k8s-1 1/1 Running 1 2d23hkube-scheduler-k8s-2 1/1 Running 1 2d22hkube-scheduler-k8s-3 1/1 Running 1 2d22h
Следующим шагом настроим отказоустойчивый IP-адрес, который
будет использоваться для доступа к Control Plane. В моем случае
было три варианта:
-
Keepalived;
-
Pacemaker;
-
kube-vip.
Здесь я потратил достаточно много времени, чтобы все завести.
Начал я с Keepalived
, который прекрасно работает в
других моих серверных системах. В общем, здесь он у меня не
завелся, не знаю в чем проблема - в Ubuntu 18.04 или в
VXLAN
-сети, которую я использую в качестве Underlay.
Tcpdump
показывал веселые VRRP
-пакетики,
летящие между k8s-{1,2,3}
, но IP-адрес вешался всеми
3мя узлами, все считали себя MASTER
-ами. Поскольку
debug ничего полезного не дал, я отказался от Keepalived и выполнил
настройку на Pacemaker
. Тут меня ждала очередная
неприятность - corosync и pacemaker не взлетали самостоятельно при
старте, несмотря на:
sudo systemctl enable corosyncsudo systemctl enable pacemaker
Я придерживаюсь такого мнения, что корневые компоненты или
работают или не надо их использовать, в /etc/rc.local
прописывать эти службы не хотелось. В итоге, я познакомился со
специализированным решением по предоставлению отказоустойчивого
IP-адреса kube-vip, разработанным
специально для Kubernetes.
Нам понадобится создать три манифеста, каждый для запуска
kube-vip на одном из узлов Control Plane:
apiVersion: v1kind: Podmetadata: creationTimestamp: null name: kube-vip-cp-k8s-1 # поменять имя, соответственно узлу namespace: kube-systemspec: nodeName: k8s-1 # будет запускаться именно на этом узле containers: - args: - start env: - name: vip_arp value: "true" - name: vip_interface value: eth1 - name: vip_leaderelection value: "true" - name: vip_leaseduration value: "5" - name: vip_renewdeadline value: "3" - name: vip_retryperiod value: "1" - name: vip_address value: 10.120.0.1 # указать реальный IP, который будет использоваться image: plndr/kube-vip:0.3.1 # проверить актуальную версию imagePullPolicy: Always name: kube-vip-cp resources: {} securityContext: capabilities: add: - NET_ADMIN - SYS_TIME volumeMounts: - mountPath: /etc/kubernetes/admin.conf name: kubeconfig - mountPath: /etc/ssl/certs name: ca-certs readOnly: true hostNetwork: true volumes: - hostPath: path: /etc/kubernetes/admin.conf name: kubeconfig - hostPath: path: /etc/ssl/certs name: ca-certsstatus: {}
Данный манифест необходимо сформировать для каждого из серверов
Control Plane и выполнить их:
kubectl apply -f cluster_config/vip-1.ymlkubectl apply -f cluster_config/vip-2.ymlkubectl apply -f cluster_config/vip-3.yml
В результате вы должны получить три нормально выполняющихся
POD-а, каждый на своем узле, при этом адрес 10.120.0.1 должен
нормально пинговаться. Проверьте, что только один из kube-vip
владеет IP:
sudo arping 10.120.0.1ARPING 10.120.0.142 bytes from 1e:01:17:00:01:22 (10.120.0.1): index=0 time=319.476 usec42 bytes from 1e:01:17:00:01:22 (10.120.0.1): index=1 time=306.360 msec42 bytes from 1e:01:17:00:01:22 (10.120.0.1): index=2 time=349.666 usec
Чем хорош kube-vip? Он не только предоставляет отказоустойчивый
IP, но и определяет когда сервер на хосте, где он выполняется
становится недоступен, переставая балансировать на него трафик.
Теперь, когда kube-vip предоставляет отказоустойчивый доступ к
Kubernetes необходимо на хостах-рекурсорах gw-1
,
gw-2
в /etc/hosts.resolv
обновить записи
для k8s-cp
:
10.120.0.1 k8s-cp
Выполните перезагрузку pdns-recursor командой sudo service
pdns-recursor restart
и проверьте, что k8s-cp
отвечает со всех узлов IP адресом 10.120.0.1
.
Проверьте, что kubectl
все еще корректно работает с
узла k8s-1
, он будет соединяться по k8s-cp, но
использовать уже другой IP-адрес.
На данном этапе у нас есть реализация отказоустойчивого Control
Plane K8S. Я рекомендую несколько раз поочередно перезагружать
k8s-{1,2,3}, чтобы проверить, что кластер остается в
работоспособном состоянии.
Добавление узла Worker-а
В нашей топологии предполагается использование двух узлов gw-1,
gw-2, на которых будет размещен Nginx Ingress и один узел общего
назначения (compute-1).
Все эти узлы в кластер можно добавить следующим образом:
kubeadm token create --print-join-command kubeadm join k8s-cp:6443 --token rn0s5p.y6waq1t6y2y6z9vw --discovery-token-ca-cert-hash sha256:0c446bfabcd99aae7e650d110f8b9d6058cac432078c4fXXXe22ec6055b4bd# ssh gw-1sudo kubeadm join k8s-cp:6443 --token rn0s5p.y6waq1t6y2y6z9vw --discovery-token-ca-cert-hash sha256:0c446bfabcd99aae7e650d110f8b9d6058cac432078c4fXXXe22ec6055b4bd# ssh gw-2...# ssh compute-1
После добавления kubectl get pds --all-namespaces
должен показать расширенный набор выполняющихся POD-ов, а
kubectl get nodes
должен вывести все узлы
кластера:
kubectl get nodesNAME STATUS ROLES AGE VERSIONcompute-1 Ready compute 2d23h v1.20.2gw-1 Ready gateway 2d4h v1.20.2gw-2 Ready gateway 2d4h v1.20.2k8s-1 Ready control-plane,master 2d23h v1.20.2k8s-2 Ready control-plane,master 2d23h v1.20.2k8s-3 Ready control-plane,master 2d23h v1.20.2
Теперь можно задать узлам роли, для того, чтобы иметь
возможность определять эти роли при развертывании приложений. В
листинге выше роли уже заданы, поскольку листинг сделан с рабочего
кластера.
Назначение ролей узлам
Роль можно присвоить просто:
kubectl label node gw-1 node-role.kubernetes.io/gateway=truekubectl label node gw-2 node-role.kubernetes.io/gateway=truekubectl label node compute-1 node-role.kubernetes.io/compute=true# если надо удалить рольkubectl label node compute-1 node-role.kubernetes.io/compute-
Настройка Ingress
Сейчас все готово для того, чтобы можно было выполнить
развертывание Nginx Ingress на узлах gw-1, gw-2. Воспользуемся
манифестом Nginx Ingress, но внесем в него ряд изменений:
-
запускать будем с сетью hostNetwork;
-
запускать будем в виде Deployment с фактором масштабирования
"2";
-
запускать будем на узлах с ролью gateway.
Разберем подробнее про hostNetwork. Дело в том, что при
использовании K8S в рамках какого-то облачного провайдера,
последний через API назначает каждому узлу External IP, который
может быть использован приложениями для связи с внешним миром. Так
вот, у нас в bare metal кластере никаких External IP нет:
kubectl get nodes --output wideNAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIMEcompute-1 Ready compute 3d v1.20.2 10.120.28.172 <none> Ubuntu 18.04.5 LTS 4.15.0-135-generic containerd://1.3.3gw-1 Ready gateway 2d4h v1.20.2 10.120.29.231 <none> Ubuntu 18.04.5 LTS 4.15.0-135-generic containerd://1.3.3gw-2 Ready gateway 2d4h v1.20.2 10.120.28.23 <none> Ubuntu 18.04.5 LTS 4.15.0-135-generic containerd://1.3.3k8s-1 Ready control-plane,master 3d v1.20.2 10.120.29.187 <none> Ubuntu 18.04.5 LTS 4.15.0-135-generic containerd://1.3.3k8s-2 Ready control-plane,master 2d23h v1.20.2 10.120.28.37 <none> Ubuntu 18.04.5 LTS 4.15.0-135-generic containerd://1.3.3k8s-3 Ready control-plane,master 2d23h v1.20.2 10.120.29.204 <none> Ubuntu 18.04.5 LTS 4.15.0-135-generic containerd://1.3.3
Собственно, назначить этот External IP можно только через API,
при этом он сбрасывается самим Kubernetes время от времени и
требует постоянной установки. В общем, это неудобно и
использоваться нормально не может. Я читал длинную переписку на
GitHub, которая закончилась ничем вразумительным, еще советуют
использовать metallb, который тоже непонятно
в каком состоянии. В общем, я решил просто завести Nginx Ingress,
используя hostNetworking, поскольку это обеспечивает привязку
данного Ingress с адресам 0.0.0.0:443
,
0.0.0.0:80
и решает мою задачу.
Собственно, манифест для запуска Nginx Ingress выглядит так:
Очень большой фрагмент YAML
apiVersion: v1kind: Namespacemetadata: name: ingress-nginx labels: app.kubernetes.io/name: ingress-nginx app.kubernetes.io/instance: ingress-nginx---# Source: ingress-nginx/templates/controller-serviceaccount.yamlapiVersion: v1kind: ServiceAccountmetadata: labels: helm.sh/chart: ingress-nginx-3.21.0 app.kubernetes.io/name: ingress-nginx app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/version: 0.43.0 app.kubernetes.io/managed-by: Helm app.kubernetes.io/component: controller name: ingress-nginx namespace: ingress-nginx---# Source: ingress-nginx/templates/controller-configmap.yamlapiVersion: v1kind: ConfigMapmetadata: labels: helm.sh/chart: ingress-nginx-3.21.0 app.kubernetes.io/name: ingress-nginx app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/version: 0.43.0 app.kubernetes.io/managed-by: Helm app.kubernetes.io/component: controller name: ingress-nginx-controller namespace: ingress-nginxdata:---# Source: ingress-nginx/templates/clusterrole.yamlapiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata: labels: helm.sh/chart: ingress-nginx-3.21.0 app.kubernetes.io/name: ingress-nginx app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/version: 0.43.0 app.kubernetes.io/managed-by: Helm name: ingress-nginxrules: - apiGroups: - '' resources: - configmaps - endpoints - nodes - pods - secrets verbs: - list - watch - apiGroups: - '' resources: - nodes verbs: - get - apiGroups: - '' resources: - services verbs: - get - list - watch - apiGroups: - extensions - networking.k8s.io # k8s 1.14+ resources: - ingresses verbs: - get - list - watch - apiGroups: - '' resources: - events verbs: - create - patch - apiGroups: - extensions - networking.k8s.io # k8s 1.14+ resources: - ingresses/status verbs: - update - apiGroups: - networking.k8s.io # k8s 1.14+ resources: - ingressclasses verbs: - get - list - watch---# Source: ingress-nginx/templates/clusterrolebinding.yamlapiVersion: rbac.authorization.k8s.io/v1kind: ClusterRoleBindingmetadata: labels: helm.sh/chart: ingress-nginx-3.21.0 app.kubernetes.io/name: ingress-nginx app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/version: 0.43.0 app.kubernetes.io/managed-by: Helm name: ingress-nginxroleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: ingress-nginxsubjects: - kind: ServiceAccount name: ingress-nginx namespace: ingress-nginx---# Source: ingress-nginx/templates/controller-role.yamlapiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata: labels: helm.sh/chart: ingress-nginx-3.21.0 app.kubernetes.io/name: ingress-nginx app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/version: 0.43.0 app.kubernetes.io/managed-by: Helm app.kubernetes.io/component: controller name: ingress-nginx namespace: ingress-nginxrules: - apiGroups: - '' resources: - namespaces verbs: - get - apiGroups: - '' resources: - configmaps - pods - secrets - endpoints verbs: - get - list - watch - apiGroups: - '' resources: - services verbs: - get - list - watch - apiGroups: - extensions - networking.k8s.io # k8s 1.14+ resources: - ingresses verbs: - get - list - watch - apiGroups: - extensions - networking.k8s.io # k8s 1.14+ resources: - ingresses/status verbs: - update - apiGroups: - networking.k8s.io # k8s 1.14+ resources: - ingressclasses verbs: - get - list - watch - apiGroups: - '' resources: - configmaps resourceNames: - ingress-controller-leader-nginx verbs: - get - update - apiGroups: - '' resources: - configmaps verbs: - create - apiGroups: - '' resources: - events verbs: - create - patch---# Source: ingress-nginx/templates/controller-rolebinding.yamlapiVersion: rbac.authorization.k8s.io/v1kind: RoleBindingmetadata: labels: helm.sh/chart: ingress-nginx-3.21.0 app.kubernetes.io/name: ingress-nginx app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/version: 0.43.0 app.kubernetes.io/managed-by: Helm app.kubernetes.io/component: controller name: ingress-nginx namespace: ingress-nginxroleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: ingress-nginxsubjects: - kind: ServiceAccount name: ingress-nginx namespace: ingress-nginx---# Source: ingress-nginx/templates/controller-service-webhook.yamlapiVersion: v1kind: Servicemetadata: labels: helm.sh/chart: ingress-nginx-3.21.0 app.kubernetes.io/name: ingress-nginx app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/version: 0.43.0 app.kubernetes.io/managed-by: Helm app.kubernetes.io/component: controller name: ingress-nginx-controller-admission namespace: ingress-nginxspec: type: ClusterIP ports: - name: https-webhook port: 443 targetPort: webhook selector: app.kubernetes.io/name: ingress-nginx app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/component: controller---# Source: ingress-nginx/templates/controller-deployment.yamlapiVersion: apps/v1kind: Deploymentmetadata: labels: helm.sh/chart: ingress-nginx-3.21.0 app.kubernetes.io/name: ingress-nginx app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/version: 0.43.0 app.kubernetes.io/managed-by: Helm app.kubernetes.io/component: controller name: ingress-nginx-controller namespace: ingress-nginxspec: replicas: 2 selector: matchLabels: app.kubernetes.io/name: ingress-nginx app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/component: controller revisionHistoryLimit: 10 minReadySeconds: 0 template: metadata: labels: app.kubernetes.io/name: ingress-nginx app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/component: controller spec: hostNetwork: true dnsPolicy: ClusterFirst containers: - name: controller image: k8s.gcr.io/ingress-nginx/controller:v0.43.0@sha256:9bba603b99bf25f6d117cf1235b6598c16033ad027b143c90fa5b3cc583c5713 imagePullPolicy: IfNotPresent lifecycle: preStop: exec: command: - /wait-shutdown args: - /nginx-ingress-controller - --election-id=ingress-controller-leader - --ingress-class=nginx - --configmap=$(POD_NAMESPACE)/ingress-nginx-controller - --validating-webhook=:8443 - --validating-webhook-certificate=/usr/local/certificates/cert - --validating-webhook-key=/usr/local/certificates/key securityContext: capabilities: drop: - ALL add: - NET_BIND_SERVICE runAsUser: 101 allowPrivilegeEscalation: true env: - name: POD_NAME valueFrom: fieldRef: fieldPath: metadata.name - name: POD_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace - name: LD_PRELOAD value: /usr/local/lib/libmimalloc.so livenessProbe: httpGet: path: /healthz port: 10254 scheme: HTTP initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 1 successThreshold: 1 failureThreshold: 5 readinessProbe: httpGet: path: /healthz port: 10254 scheme: HTTP initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 1 successThreshold: 1 failureThreshold: 3 ports: - name: http containerPort: 80 protocol: TCP - name: https containerPort: 443 protocol: TCP - name: webhook containerPort: 8443 protocol: TCP volumeMounts: - name: webhook-cert mountPath: /usr/local/certificates/ readOnly: true resources: requests: cpu: 100m memory: 90Mi nodeSelector: node-role.kubernetes.io/gateway: "true" serviceAccountName: ingress-nginx terminationGracePeriodSeconds: 300 volumes: - name: webhook-cert secret: secretName: ingress-nginx-admission---# Source: ingress-nginx/templates/admission-webhooks/validating-webhook.yaml# before changing this value, check the required kubernetes version# https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#prerequisitesapiVersion: admissionregistration.k8s.io/v1kind: ValidatingWebhookConfigurationmetadata: labels: helm.sh/chart: ingress-nginx-3.21.0 app.kubernetes.io/name: ingress-nginx app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/version: 0.43.0 app.kubernetes.io/managed-by: Helm app.kubernetes.io/component: admission-webhook name: ingress-nginx-admissionwebhooks: - name: validate.nginx.ingress.kubernetes.io matchPolicy: Equivalent rules: - apiGroups: - networking.k8s.io apiVersions: - v1beta1 operations: - CREATE - UPDATE resources: - ingresses failurePolicy: Fail sideEffects: None admissionReviewVersions: - v1 - v1beta1 clientConfig: service: namespace: ingress-nginx name: ingress-nginx-controller-admission path: /networking/v1beta1/ingresses---# Source: ingress-nginx/templates/admission-webhooks/job-patch/serviceaccount.yamlapiVersion: v1kind: ServiceAccountmetadata: name: ingress-nginx-admission annotations: helm.sh/hook: pre-install,pre-upgrade,post-install,post-upgrade helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded labels: helm.sh/chart: ingress-nginx-3.21.0 app.kubernetes.io/name: ingress-nginx app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/version: 0.43.0 app.kubernetes.io/managed-by: Helm app.kubernetes.io/component: admission-webhook namespace: ingress-nginx---# Source: ingress-nginx/templates/admission-webhooks/job-patch/clusterrole.yamlapiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata: name: ingress-nginx-admission annotations: helm.sh/hook: pre-install,pre-upgrade,post-install,post-upgrade helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded labels: helm.sh/chart: ingress-nginx-3.21.0 app.kubernetes.io/name: ingress-nginx app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/version: 0.43.0 app.kubernetes.io/managed-by: Helm app.kubernetes.io/component: admission-webhookrules: - apiGroups: - admissionregistration.k8s.io resources: - validatingwebhookconfigurations verbs: - get - update---# Source: ingress-nginx/templates/admission-webhooks/job-patch/clusterrolebinding.yamlapiVersion: rbac.authorization.k8s.io/v1kind: ClusterRoleBindingmetadata: name: ingress-nginx-admission annotations: helm.sh/hook: pre-install,pre-upgrade,post-install,post-upgrade helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded labels: helm.sh/chart: ingress-nginx-3.21.0 app.kubernetes.io/name: ingress-nginx app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/version: 0.43.0 app.kubernetes.io/managed-by: Helm app.kubernetes.io/component: admission-webhookroleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: ingress-nginx-admissionsubjects: - kind: ServiceAccount name: ingress-nginx-admission namespace: ingress-nginx---# Source: ingress-nginx/templates/admission-webhooks/job-patch/role.yamlapiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata: name: ingress-nginx-admission annotations: helm.sh/hook: pre-install,pre-upgrade,post-install,post-upgrade helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded labels: helm.sh/chart: ingress-nginx-3.21.0 app.kubernetes.io/name: ingress-nginx app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/version: 0.43.0 app.kubernetes.io/managed-by: Helm app.kubernetes.io/component: admission-webhook namespace: ingress-nginxrules: - apiGroups: - '' resources: - secrets verbs: - get - create---# Source: ingress-nginx/templates/admission-webhooks/job-patch/rolebinding.yamlapiVersion: rbac.authorization.k8s.io/v1kind: RoleBindingmetadata: name: ingress-nginx-admission annotations: helm.sh/hook: pre-install,pre-upgrade,post-install,post-upgrade helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded labels: helm.sh/chart: ingress-nginx-3.21.0 app.kubernetes.io/name: ingress-nginx app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/version: 0.43.0 app.kubernetes.io/managed-by: Helm app.kubernetes.io/component: admission-webhook namespace: ingress-nginxroleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: ingress-nginx-admissionsubjects: - kind: ServiceAccount name: ingress-nginx-admission namespace: ingress-nginx---# Source: ingress-nginx/templates/admission-webhooks/job-patch/job-createSecret.yamlapiVersion: batch/v1kind: Jobmetadata: name: ingress-nginx-admission-create annotations: helm.sh/hook: pre-install,pre-upgrade helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded labels: helm.sh/chart: ingress-nginx-3.21.0 app.kubernetes.io/name: ingress-nginx app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/version: 0.43.0 app.kubernetes.io/managed-by: Helm app.kubernetes.io/component: admission-webhook namespace: ingress-nginxspec: template: metadata: name: ingress-nginx-admission-create labels: helm.sh/chart: ingress-nginx-3.21.0 app.kubernetes.io/name: ingress-nginx app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/version: 0.43.0 app.kubernetes.io/managed-by: Helm app.kubernetes.io/component: admission-webhook spec: containers: - name: create image: docker.io/jettech/kube-webhook-certgen:v1.5.1 imagePullPolicy: IfNotPresent args: - create - --host=ingress-nginx-controller-admission,ingress-nginx-controller-admission.$(POD_NAMESPACE).svc - --namespace=$(POD_NAMESPACE) - --secret-name=ingress-nginx-admission env: - name: POD_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace restartPolicy: OnFailure serviceAccountName: ingress-nginx-admission securityContext: runAsNonRoot: true runAsUser: 2000---# Source: ingress-nginx/templates/admission-webhooks/job-patch/job-patchWebhook.yamlapiVersion: batch/v1kind: Jobmetadata: name: ingress-nginx-admission-patch annotations: helm.sh/hook: post-install,post-upgrade helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded labels: helm.sh/chart: ingress-nginx-3.21.0 app.kubernetes.io/name: ingress-nginx app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/version: 0.43.0 app.kubernetes.io/managed-by: Helm app.kubernetes.io/component: admission-webhook namespace: ingress-nginxspec: template: metadata: name: ingress-nginx-admission-patch labels: helm.sh/chart: ingress-nginx-3.21.0 app.kubernetes.io/name: ingress-nginx app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/version: 0.43.0 app.kubernetes.io/managed-by: Helm app.kubernetes.io/component: admission-webhook spec: containers: - name: patch image: docker.io/jettech/kube-webhook-certgen:v1.5.1 imagePullPolicy: IfNotPresent args: - patch - --webhook-name=ingress-nginx-admission - --namespace=$(POD_NAMESPACE) - --patch-mutating=false - --secret-name=ingress-nginx-admission - --patch-failure-policy=Fail env: - name: POD_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace restartPolicy: OnFailure serviceAccountName: ingress-nginx-admission securityContext: runAsNonRoot: true runAsUser: 2000
Запустив его с помощью kubectl apply -f
nginx-ingress.yaml
, мы получим два POD-а Nginx,
выполняющихся на узлах gw-1
, gw-2
и
слушающих 443-й и 80-й порты:
kubectl get pods --all-namespaces | grep nginxingress-nginx ingress-nginx-admission-create-4mm9m 0/1 Completed 0 46hingress-nginx ingress-nginx-admission-patch-7jkwg 0/1 Completed 2 46hingress-nginx ingress-nginx-controller-b966cf6cd-7kpzm 1/1 Running 1 46hingress-nginx ingress-nginx-controller-b966cf6cd-ckl97 1/1 Running 0 46h
На узлах gw-1
, gw-2
:
sudo netstat -tnlp | grep -E ':(443|80)'tcp 0 0 0.0.0.0:443 0.0.0.0:* LISTEN 2661/nginx: master tcp 0 0 0.0.0.0:443 0.0.0.0:* LISTEN 2661/nginx: master tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 2661/nginx: master tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 2661/nginx: master tcp6 0 0 :::443 :::* LISTEN 2661/nginx: master tcp6 0 0 :::443 :::* LISTEN 2661/nginx: master tcp6 0 0 :::80 :::* LISTEN 2661/nginx: master tcp6 0 0 :::80 :::* LISTEN 2661/nginx: master
Можно постучаться на публичные адреса gw-1
,
gw-2
по 80-му порту и получить приветственную страницу
Nginx. Далее, Вы можете использовать публичные адреса
gw-1
, gw-2
для создания записей DNS,
использования в CDN и т.п.
Протестировать работу inress можно, создав сервис, на который
Ingress будет проксировать трафик (взято отсюда) -
echo1.yaml
:
apiVersion: v1kind: Servicemetadata: name: echo1spec: ports: - port: 80 targetPort: 5678 selector: app: echo1---apiVersion: apps/v1kind: Deploymentmetadata: name: echo1spec: selector: matchLabels: app: echo1 replicas: 2 template: metadata: labels: app: echo1 spec: containers: - name: echo1 image: hashicorp/http-echo args: - "-text=echo1" ports: - containerPort: 5678
Выполните данный манифест с помощью kubectl apply -f
echo1.yaml
. Теперь создадим правило Ingress
(ingress-echo1.yaml):
apiVersion: networking.k8s.io/v1beta1kind: Ingressmetadata: name: echo-ingressspec: rules: - host: echo1.example.com http: paths: - backend: serviceName: echo1 servicePort: 80
Выполним данный манифест kubectl apply -f
ingress-echo1.yaml
. Теперь, если на локальном компьютере в
/etchosts
внести запись для echo1.example.com:
127.0.0.1localhost127.0.1.1manager# The following lines are desirable for IPv6 capable hosts::1 localhost ip6-localhost ip6-loopbackff02::1 ip6-allnodesff02::2 ip6-allroutersX.Y.Z.C echo1.example.com
То можно получить проксирование трафика через Nginx Ingress.
Проверим с помощью curl:
curl echo1.example.comecho1
Установка K8S Dashboard
Для установки Dashboard необходимо выполнить следующую
команду:
kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0/aio/deploy/recommended.yaml
Dashboard имеет ряд ограничений, например, он не работает через
http, если обращение осуществляется не с localhost. В целом, не
рекомендуется как-либо предоставлять доступ к Dashboard извне
кластера через Ingress. Кроме того, Dashboard использует встроенную
систему RBAC Kubernetes, поэтому требуется создать пользователя и
дать ему права на Dashboard. Инструкция взята с этой страницы, здесь
приводится для простоты восприятия.
Создадим аккаунт:
cat <<EOF | kubectl apply -f -apiVersion: v1kind: ServiceAccountmetadata: name: admin-user namespace: kubernetes-dashboardEOF
Определим роль пользователя:
cat <<EOF | kubectl apply -f -apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRoleBindingmetadata: name: admin-userroleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cluster-adminsubjects:- kind: ServiceAccount name: admin-user namespace: kubernetes-dashboardEOF
Получим токен, с помощью которого пользователь сможет войти в
Dashboard:
kubectl -n kubernetes-dashboard get secret $(kubectl -n kubernetes-dashboard get sa/admin-user -o jsonpath="{.secrets[0].name}") -o go-template="{{.data.token | base64decode}}"
Если на вашей машине уже настроен kubect
и есть
$HOME/.kube/config
, а сама машина "видит" k8s-cp, то
вы можете запустить kubectl proxy
и получить доступ к
Dashboard по ссылке: http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/.
Если же ваша машина с браузером находится вне кластера, то
переходим к следующему шагу, где мы настроим проброс API Kubernetes
наружу через HAProxy.
Доступ к API K8S из внешней сети
На узлах gw-1
, gw-2
необходимо
установить haproxy
. После установки измените
конфигурационный файл /etc/haproxy/haproxy.cfg
так,
чтобы далее секции defaults
он выглядел следующим
образом:
defaults # mode is inherited by sections that follow mode tcpfrontend k8s # receives traffic from clients bind :6443 default_backend kubernetesbackend kubernetes # relays the client messages to servers server k8s k8s-cp:6443
Теперь вы можете обратиться к API K8S извне, указав в
/etc/hosts
своей локальной машины адрес
gw-1
или gw-2
в качестве
k8s-cp
. Вам должны быть доступны с локальной машины
все команды kubectl, включая kubectl proxy:
kubectl proxyStarting to serve on 127.0.0.1:8001
Можно открыть в браузере http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/
и насладиться видом приглашения авторизации Dashboard K8S:
Вводим токен, полученный с помощью:
kubectl -n kubernetes-dashboard get secret $(kubectl -n kubernetes-dashboard get sa/admin-user -o jsonpath="{.secrets[0].name}") -o go-template="{{.data.token | base64decode}}"eyJhbGciOiJSUzI1NiIsImtpZCI6IlFkcGxwMTN2YlpyNS1TOTYtUnVYdsfadfsdjfksdlfjm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlcm5ldGVzLWRhc2hib2FyZCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJhZG1pbi11c2VyLXRva2VuLWd6anprIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImFkbWluLXVzZXIiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiJjM2RiOWFkMS0yYjdmLTQ3YTYtOTM3My1hZWI2ZjJkZjk0NTAiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6a3ViZXJuZXRlcy1kYXNoYm9hcmQ6YWRtaW4tdXNlciJ9.ht-RyqLXY4UzBnxIzJcnQn8Kk2iHWZzKVeAjUhKkJ-vOAtyqBu50rExgiRWsEHWfJp1p9xN8sXFKg62FFFbHWftPf6PmwfcBK2lXPy6OL9OaooP17WJVn75KKXIZHPWLO9VmRXPB1S-v2xFuG_J8jHB8pZHJlFjp4zIXfB--QwhrxeoTt5zv3CfXCl1_VYgCoqaxRsa7e892-vtMijBo7EZfJiyuAUQr_TsAIDY3zOWFJeHbwPFWzL_1fF9Y2o3r0pw7hYZHUoI8Z-3hbfOi10QBRyQlNZTMFSh7Z38RRbV1tw2ZmMvgSQyHa9TZFy2kYik6VnugNB2cilamo_b7hg
и переходим на главный экран Dashboard:
Вместо заключения
Как я писал в начале статьи, у меня уже есть опыт развертывания
оркестраций на базе Docker, Apache Mesos (DC/OS), тем не менее,
документация Kubernetes мне показалась сложной, запутанной и
фрагментарной. Чтобы получить текущий результат я прошерстил
довольном много сторонних руководств, Issue GitHub и, конечно,
документацию Kubernetes. Надеюсь, что данное руководство поможет
вам сэкономить свое время.