Od kilku lat Kubernetes udowadnia, że jest najlepszym oprogramowaniem do orkiestracji kontenerów na rynku. Dziś wszyscy trzej największy dostawcy chmury (Amazon, Google i Azure) oferują zarządzane klastry Kubernetes: odpowiednio EKS, GKE i AKS. Wszystkie te usługi są gotowe do produkcji, w pełni zintegrowane z innymi usługami chmurowymi i objęte wsparciem komercyjnym.
Niestety pojawia się jeden problem — cena. Zazwyczaj takie usługi chmurowe są skierowane do dużych organizacji z dużym budżetem. Przykładowo Amazon Elastic Kubernetes Service kosztuje 144 $/mies. tylko za zarządzanie klastrem („control plane”), a wszystkie węzły obliczeniowe i pamięć masowa są rozliczane osobno. Google Kubernetes Engine nie pobiera opłaty za control plane, ale instancje dla węzłów też nie są tanie — rozsądna maszyna 4 GiB z jednym rdzeniem kosztuje 24 $/mies. Pytanie brzmi: czy można to zrobić taniej? Ponieważ Kubernetes jest w 100 % open source, możemy zainstalować go na własnym serwerze. Ile zaoszczędzimy? Duzi tradycyjni dostawcy VPS (jak OVH czy Linode) oferują przyzwoite maszyny w cenach zaczynających się od 5 $/mies. Czy to zadziała? Sprawdźmy!
Konfiguracja serwera
Sprzęt
Do uruchomienia naszego klastra z jednym węzłem i jednym kontrolerem nie potrzebujemy niczego wyszukanego. Oficjalne wymagania dla klastra zakładanego narzędziem kubeadm
to:
- 2 GiB RAM
- 2 rdzenie CPU
- wystarczająco duży dysk na system operacyjny, binaria Kubernetes oraz obrazy Docker
W tym poradniku użyję minimalnych rekomendowanych parametrów, choć w praktyce można to odpalić nawet na słabszym sprzęcie. Tworzymy zatem niewielki serwer wirtualny i logujemy się do niego przez SSH (używam DigitalOcean, który przy rejestracji przez GitHub Student Program daje 50 $ na rok):
$ doctl compute droplet create kubernetes-for-poor \
--image ubuntu-16-04-x64 \
--size 2gb \
--region fra1 \
--ssh-keys ee:a4:d1:c3:61:b1:7c:33:ce:49:a0:01:c5:a4:3b:09
(...)
$ doctl compute ssh kubernetes-for-poor
Jeśli chcemy dodatkowo zoptymalizować, warto wyłączyć niepotrzebne usługi, odinstalować zbędne pakiety, usunąć nieużywane lokalizacje itp. My jednak pójdziemy domyślną ścieżką Ubuntu 16.04 od DigitalOcean.
Instalacja Dockera i kubeadm
Tu będziemy podążać za oficjalnym przewodnikiem kubeadm
, narzędzia do bootstrapowania klastra na dowolnej maszynie Linux.
Instalacja Dockera:
$ apt-get update
$ apt-get install -y docker.io
Sprawdźmy instalację:
$ docker run --rm -ti hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
9db2ca6ccae0: Pull complete
Digest: sha256:4b8ff392a12ed9ea17784bd3c9a8b1fa32...
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
This message shows that your installation appears to be working correctly.
(...)
$
Instalacja kubeadm, kubelet i kubectl:
$ apt-get update && apt-get install -y apt-transport-https curl
$ curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg \
| apt-key add -
$ cat </etc/apt/sources.list.d/kubernetes.list
deb http://apt.kubernetes.io/ kubernetes-xenial main
EOF
$ apt-get update
$ apt-get install -y kubelet kubeadm kubectl
$ apt-mark hold kubelet kubeadm kubectl
Konfiguracja węzła master
Teraz faktycznie uruchomimy Kubernetes na naszym serwerze. Dwa poniższe polecenia inicjalizują węzeł master i wybierają Flannel jako sieć dla podów:
$ kubeadm init --apiserver-advertise-address=$PUBLIC_IP \
--pod-network-cidr=10.244.0.0/16
[init] using Kubernetes version: v1.11.2
[preflight] running pre-flight checks
(...)
Your Kubernetes master has initialized successfully!
$ mkdir -p $HOME/.kube
$ sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
$ sudo chown $(id -u):$(id -g) $HOME/.kube/config
$ kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/v0.10.0/Documentation/kube-flannel.yml
To może zająć kilka minut. Możesz śledzić postęp:
$ watch kubectl get pods --all-namespaces
NAMESPACE NAME READY STATUS
kube-system coredns-78fcdf6894-dhfnl 1/1 Running
kube-system coredns-78fcdf6894-kf2vl 1/1 Running
kube-system etcd-kubernetes-for-cheap 1/1 Running
kube-system kube-apiserver-kubernetes-for-cheap 1/1 Running
kube-system kube-controller-manager-kuber... 1/1 Running
kube-system kube-flannel-ds-lx5ft 1/1 Running
kube-system kube-proxy-6nszm 1/1 Running
kube-system kube-scheduler-kubernetes-for-cheap 1/1 Running
Gdy wszystkie pody będą w stanie Running, klaster jest gotowy. Domyślnie master nie przydziela na siebie workloadów — usuwamy więc tę etykietę:
$ kubectl taint nodes --all node-role.kubernetes.io/master-
Testowanie klastra
Test harmonogramu podów
$ kubectl run --rm --restart=Never -ti --image=hello-world my-test-pod
(...)
Hello from Docker!
This message shows that your installation appears to be working correctly.
(...)
pod "my-test-pod" deleted
Test sieci
Uruchomimy serwer Apache przez Deployment i Service typu NodePort:
$ cat apache-nodeport.yml
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: apache-nodeport-test
spec:
selector:
matchLabels:
app: apache-nodeport-test
replicas: 1
template:
metadata:
labels:
app: apache-nodeport-test
spec:
containers:
- name: apache
image: httpd
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: apache-nodeport-test
spec:
selector:
app: apache-nodeport-test
type: NodePort
ports:
- port: 80
$ kubectl apply -f apache-nodeport.yml
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
apache-nodeport-test-79c84b9fbb-flc9p 1/1 Running 0 25s
$ kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
apache-nodeport-test NodePort 10.108.114.13 80:31456/TCP
kubernetes ClusterIP 10.96.0.1 443/TCP
$ curl http://139.59.211.151:31456
It works!
Jak przetrwać w świecie Kubernetes poza chmurą zarządzaną
Sieć Kubernetes poza chmurą
W chmurze tworzy się Service typu LoadBalancer (ELB za min. 20 $/mies.), ale w self-hostingu musimy kombinować. Można użyć NodePort + proxy Nginx, ale lepiej wdrożyć Ingress Controller (np. Nginx Ingress) z hostPort na 80 i 443.
$ kubectl apply -f .
(...)
$ curl localhost
default backend - 404
Pamięć masowa poza chmurą
Kontenery są efemeryczne — Docker oferuje wolumeny hostpath, ale tracimy PVC. Rozwiązaniem jest „hostpath provisioner”, który udostępnia katalogi hosta przez PVC.
$ kubectl apply -f .
$ cat test-pvc.yml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: test-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 200Mi
$ kubectl apply -f test-pvc.yml
$ kubectl get pvc
test-pvc Bound pvc-a5109ca0... 200Mi RWO hostpath 5s
Pod testujący zapis:
$ cat test-pvc-pod.yml
apiVersion: v1
kind: Pod
metadata:
name: myapp-pod
spec:
containers:
- name: myapp-container
image: ubuntu
command: ['bash', '-c', 'while true; do sleep 1 && echo IT_WORKS | tee -a /my-volume/test.txt; done']
volumeMounts:
- mountPath: /my-volume
name: test
volumes:
- name: test
persistentVolumeClaim:
claimName: test-pvc
$ kubectl apply -f test-pvc-pod.yml
(...)
$ ls /var/kubernetes
default-test-pvc-pvc-a5109ca0-b289-11e8-bc89-fa163e350fbc
$ cat /var/kubernetes/default-test-pvc-pvc-a5109ca0-b289-11e8-bc89-fa163e350fbc/test.txt
IT_WORKS
IT_WORKS
(...)
Deploy przykładowej aplikacji
Wdrożymy WordPressa z Ingress zamiast LoadBalancer:
$ cat wp.yml
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql-pv-claim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: wordpress-mysql
spec:
selector:
matchLabels:
app: wordpress-mysql
strategy:
type: Recreate
template:
metadata:
labels:
app: wordpress-mysql
spec:
containers:
- image: mysql:5.6
name: mysql
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-pass
key: password
ports:
- containerPort: 3306
name: mysql
volumeMounts:
- name: mysql-persistent-storage
mountPath: /var/lib/mysql
volumes:
- name: mysql-persistent-storage
persistentVolumeClaim:
claimName: mysql-pv-claim
---
apiVersion: v1
kind: Service
metadata:
name: wordpress-mysql
spec:
ports:
- port: 3306
selector:
app: wordpress-mysql
---
apiVersion: v1
kind: Service
metadata:
name: wordpress-web
labels:
app: wordpress-web
spec:
ports:
- port: 80
selector:
app: wordpress-web
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: wp-pv-claim
labels:
app: wordpress
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: wordpress-web
spec:
selector:
matchLabels:
app: wordpress-web
strategy:
type: Recreate
template:
metadata:
labels:
app: wordpress-web
spec:
containers:
- image: wordpress:4.8-apache
name: wordpress
env:
- name: WORDPRESS_DB_HOST
value: wordpress-mysql
- name: WORDPRESS_DB_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-pass
key: password
ports:
- containerPort: 80
name: wordpress
volumeMounts:
- name: wordpress-persistent-storage
mountPath: /var/www/html
volumes:
- name: wordpress-persistent-storage
persistentVolumeClaim:
claimName: wp-pv-claim
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: wordpress-web
spec:
rules:
- host: kubernetes-for-poor.test
http:
paths:
- path: /
backend:
serviceName: wordpress-web
servicePort: 80
$ kubectl create secret generic mysql-pass --from-literal=password=sosecret
$ kubectl apply -f wp.yml
Podsumowanie
I to wszystko! W pełni funkcjonalny, jednostanowiskowy klaster Kubernetes — idealny do osobistych projektów. Czy to overkill dla kilku niekrytycznych stron? Być może, ale najważniejszy jest proces nauki.