We wstępnie zapoznaliśmy się z firmą Hetzner oraz z plusami usług tej firmy. Teraz czas, aby przejść do sedna, czyli jak postawić klaster k8s na tej platformie. Zaczynamy… Wchodzimy na stronę https://hetzner.cloud/?ref=CM3Ec8fo6mU6 (jest to link polecający, który da Ci na start 20 €), następnie Login -> Cloud -> Register now.

Po utworzeniu konta, logujemy się i tworzymy nowy projekt. Ja swój nazwałem k8s. Zamykanie infrastruktury w projekty znacznie ułatwi kontrolę kosztów.

Następnie w nowo utworzonym projekcie przechodzimy do Security -> Api tokens -> Generate API token -> nadajemy mu jakąś nazwę i wybieramy opcję „Read & Write”. Wygenerowany klucz dodajemy jako zmienną.

HCLOUD_TOKEN="<YOUR_TOKEN>"

Instalujemy znakomite narzędzie do obsługi Hetzner Cloud. Dzięki niemu nie będziemy musieli już zaglądać na ich stronę. W zależności od Twojego systemu instalacja będzie wyglądać nieco inaczej. Wszystkie potrzebne informacje są na oficjalnej stronie: https://github.com/hetznercloud/cli#installation

Logujemy się do Hetzner Cloud przy pomocy nowego tokenu.

hcloud context create <PROJECT_NAME>

Dodajmy nasz klucz publiczny. Jest to ważne (i zwiększa bezpieczeństwo), ponieważ ten klucz zostanie automatycznie dodany na każdy nowo utworzony serwer. Dzięki temu firma Hetzner nie wyśle nam maila z hasłem roota, a zalogujemy się na serwer swoim kluczem. Jeżeli nie wiesz, jak utworzyć swoją parę kluczy, zrób to według na przykład tej instrukcji https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent

hcloud ssh-key create --name <YOUR_NAME>@<MACHINE_NAME> --public-key-from-file ~/.ssh/id_rsa.pub

Na początku utworzymy sieć, z której nowe serwery dostaną prywatne adresy IP. Do tej sieci podepniemy nasze serwery, dzięki temu komunikacja pomiędzy nimi odbywać się będzie tylko w obrębie tej prywatnej sieci i nie wyjdzie na zewnątrz.

hcloud network create --name $(hcloud context active) --ip-range 10.98.0.0/16 && \
hcloud network add-subnet $(hcloud context active) --network-zone eu-central --type server --ip-range 10.98.0.0/16

Tworzymy serwery, z których będzie się składał nasz klaster. Jak pisałem we wstępie, mój klaster składa się z jednego node master (CX11 – 1 CPU, 2 GB RAM) oraz trzech worker node (CX21 – 2 CPU, 4 GB RAM). Jako lokalizację wybrałem Niemcy, Falkenstein (fsn1-dc14), jako że jest najbliżej Polski. Polecenia te automatycznie utworzą serwery z systemem Ubuntu 20.04 i dodadzą wcześniej utworzony klucz i sieć.

hcloud server create --type cx11 --datacenter fsn1-dc14 --name master-1 --image ubuntu-20.04 --ssh-key $(hcloud ssh-key list -o noheader -o columns=id) --network $(hcloud network list -o noheader -o columns=id) && \
 hcloud server create --type cx21 --datacenter fsn1-dc14 --name worker-1 --image ubuntu-20.04 --ssh-key $(hcloud ssh-key list -o noheader -o columns=id) --network $(hcloud network list -o noheader -o columns=id) && \
 hcloud server create --type cx21 --datacenter fsn1-dc14 --name worker-2 --image ubuntu-20.04 --ssh-key $(hcloud ssh-key list -o noheader -o columns=id) --network $(hcloud network list -o noheader -o columns=id) && \
 hcloud server create --type cx21 --datacenter fsn1-dc14 --name worker-3 --image ubuntu-20.04 --ssh-key $(hcloud ssh-key list -o noheader -o columns=id) --network $(hcloud network list -o noheader -o columns=id)

Dla bezpieczeństwa najpierw uaktualnimy wszystkie pakiety do najnowszych wersji. Polecenie to loguje się do każdego serwera przez ssh i uaktualnia cały system.

hcloud server list -o columns=ipv4 -o noheader \
| while read -r ip; \
do printf "#######\nUpdating $ip\n#######\n"; \
ssh -n root@$ip apt-get update; \
ssh -n root@$ip apt-get -y dist-upgrade; \
ssh -n root@$ip reboot; \
done

Musimy się upewnić, że klaster może się komunikować pomiędzy nodami i podami.

hcloud server list -o columns=ipv4 -o noheader \
 | while read -r ip; \
 do printf "#######\nUpdating $ip\n#######\n"; \
 printf "# Allow IP forwarding for kubernetes\nnet.bridge.bridge-nf-call-iptables = 1\nnet.ipv4.ip_forward = 1\n" \
 | ssh root@$ip -T "cat > /etc/sysctl.conf"; \
 ssh -n root@$ip modprobe br_netfilter; \
 ssh -n root@$ip sysctl -p; \
 done

Przygotowujemy konfigurację kubleta, by w przyszłości używał Cloud Controller Manager, który zintegruje klaster z Hetzner API.

hcloud server list -o columns=ipv4 -o noheader \
| while read -r ip; \
do printf "#######\nUpdating $ip\n#######\n"; \
ssh -n root@$ip mkdir /etc/systemd/system/kubelet.service.d; \
printf "[Service]\nEnvironment=\"KUBELET_EXTRA_ARGS=--cloud-provider=external\"\n" \
| ssh root@$ip -T "cat > /etc/systemd/system/kubelet.service.d/20-hetzner-cloud.conf"; \
done

Następnie instalujemy Dockera (wersja 19.03.15) i Kubernetes (wersja 1.19.12). Dla pewności, gdyby przyszłe wersje były niekompatybilne z implementacjami CNI lub CSI Hetznera.

hcloud server list -o columns=ipv4 -o noheader \
| while read -r ip; \
do printf "#######\nUpdating $ip\n#######\n"; \
ssh -n root@$ip mkdir /etc/systemd/system/docker.service.d; \
printf "[Service]\nExecStart=\nExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock --exec-opt native.cgroupdriver=systemd\n" \
| ssh root@$ip -T "cat > /etc/systemd/system/docker.service.d/00-cgroup-systemd.conf"; \
ssh -n root@$ip systemctl daemon-reload; \
done
hcloud server list -o columns=ipv4 -o noheader \
| while read -r ip; \
do printf "#######\nUpdating $ip\n#######\n"; \
hcloud server list -o columns=ipv4 -o noheader \
 | while read -r ip; \
 do printf "#######\nUpdating $ip\n#######\n"; \
 ssh -n root@$ip "curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -"; \
 ssh -n root@$ip "curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -"; \
 printf "deb [arch=amd64] https://download.docker.com/linux/ubuntu focal stable\ndeb http://packages.cloud.google.com/apt/ kubernetes-xenial main\n" \
 | ssh root@$ip -T "cat > /etc/apt/sources.list.d/docker-and-kubernetes.list"; \
 ssh -n root@$ip apt-get update; \
 ssh -n root@$ip apt-get install -y docker-ce=5:19.03.15~3-0~ubuntu-focal kubeadm=1.19.12-00 kubectl=1.19.12-00 kubelet=1.19.12-00; \
 done

Po wszystkim musimy się zalogować na serwer master i zainicjować klaster.

ssh -n root@$(hcloud server list | grep master | awk '{print $4}') kubeadm config images pull; \
 ssh -n root@$(hcloud server list | grep master | awk '{print $4}') kubeadm init \
   --service-cidr=10.96.0.0/16 \
   --pod-network-cidr=10.244.0.0/16 \
   --kubernetes-version=v1.19.12 \
   --ignore-preflight-errors=NumCPU \
   --apiserver-cert-extra-sans=$(hcloud server describe -o json master-1 | jq -r .private_net[0].ip)

Pod koniec polecenia kubeadm join na masterze została wyświetlona komenda w formacie podanym poniżej. Należy ją skopiować, przyda nam się później do podłączenia worker nodów.

kubeadm join <MASTER_NODE_IP>:6443 --token <TOKEN> \
    --discovery-token-ca-cert-hash sha256:<CA-CERT-HASH>

Następnie kopiujemy na lokalną maszynę konfigurację potrzebną do umożliwienia komunikacji z klastrem. Dzięki temu kolejne polecenia kubectl można już przeprowadzać z poziomu lokalnej maszyny.

scp root@$(hcloud server list | grep master | awk '{print $4}'):/etc/kubernetes/admin.conf ${HOME}/.kube/config
$ kubectl get nodes

NAME       STATUS     ROLES    AGE     VERSION
master-1   NotReady   master   5m28s   v1.19.12

Master jest na miejscu, ale ma status NotReady – dlaczego? Ponieważ nie skonfigurowaliśmy sieci potrzebnych by nody mogły się komunikować. W naszym przypadku CNI (Container Network Interface) będzie implementował flannel. Instalujemy go poleceniem:

kubectl apply -f https://github.com/coreos/flannel/raw/master/Documentation/kube-flannel.yml

Teraz jesteśmy gotowi, by zalogować się na worker nody i użyć komendy zapisanej po wykonaniu kubectl init. Jeżeli zapomniałeś jej zapisać, można ją odzyskać, wykonując polecenie poniżej na master nodzie.

kubeadm token create --print-join-command

Polecenie będzie mieć postać

hcloud server list | grep worker | awk '{print $4}' \
 | while read -r ip; \
 do printf "#######\nUpdating $ip\n#######\n"; \
 ssh -n root@$ip kubeadm join <MASTER_NODE_IP>:6443 --token <TOKEN> \
    --discovery-token-ca-cert-hash sha256:<CA-CERT-HASH>; \
 done

Po wszystkim jeszcze raz sprawdzamy stan naszych nodów, wygląda lepiej, ale po bliższym przyjrzeniu zobaczymy, że klaster nie jest jeszcze w pełni gotowy do działania.

$ kubectl get nodes
NAME       STATUS   ROLES    AGE     VERSION
master-1   Ready    master   8m38s   v1.19.12
worker-1   Ready    <none>   6m32s   v1.19.12
worker-2   Ready    <none>   6m14s   v1.19.12
worker-3   Ready    <none>   5m42s   v1.19.12
$ kubectl describe node worker-1 | grep Taints
 Taints:             node.cloudprovider.kubernetes.io/uninitialized=true:NoSchedule

Controller-manager dodał flagę „uninitialized”, ponieważ uznał, że nody nie są jeszcze gotowe. Aby ten stan się zmienił sieci oraz DNS muszą działać poprawne, aktualnie coredns czeka na noda, gdzie może zostać zainstalowany. Musimy na nim wymusić, aby zaakceptował niezainicjalizowane nody.

$ kubectl get po -n kube-system | grep coredns
 coredns-f9fd979d6-8dk48            0/1     Pending   0          35m
 coredns-f9fd979d6-x6c9z            0/1     Pending   0          35m

kubectl -n kube-system patch daemonset kube-flannel-ds --type json -p '[{"op":"add","path":"/spec/template/spec/tolerations/-","value":{"key":"node.cloudprovider.kubernetes.io/uninitialized","value":"true","effect":"NoSchedule"}}]' && \
kubectl -n kube-system patch deployment coredns --type json -p '[{"op":"add","path":"/spec/template/spec/tolerations/-","value":{"key":"node.cloudprovider.kubernetes.io/uninitialized","value":"true","effect":"NoSchedule"}}]'

W kolejnej części pokażę Ci, jak skonfigurować sieć, tak by wystawić nasz klaster na świat zewnętrzny oraz sprawdzimy, czy uda nam się stworzyć automatycznie volumen z danymi na serwerach Hetzner. Jeżeli podobał Ci się ten artykuł, daj znać w komentarzu.


0 komentarzy

Dodaj komentarz

Avatar placeholder

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *