Einfacher Weg zu eigenem Kubernetes-Cluster mit RKE
RKE
Rancher Kubernetes Engine ist ein schlankes, schnelles und sehr einfaches Tool zum Aufsetzen von Kubernetes-Clustern.
In der folgenden Zusammenfassung wird beschrieben wie ein Cluster beispielhaft konfiguriert werden kann samt eines Zertifikatsmanagers, Logging, Monitoring, Alerting, sowie eines eigenen Storage-Provisioner. Es werden nicht viele Hintergrundinformationen erläutert. Der Fokus liegt auf dem Setup selbst.
Server
Für den Kubernetes-Cluster wurden drei vServer mit Ubuntu 18.04 und Docker-CE 18.09 vorbereitet. Swap wurde deaktiviert. Jeder Server beistzt 2 IP's, eine interne und eine externe. Eine Key-Authorisierung ist vom Rechner, auf dem Kubectl und RKE installiert und ausgeführt werden auf allen 3 Nodes konfiguriert.
Cluster-Setup
Im ersten Schritt der Cluster-Installation wird das RKE-Script heruntergeladen und ausführbar gemacht:
wget https://github.com/rancher/rke/releases/download/v0.1.18/rke_linux-amd64
mv rke_linux-amd64 rke
chmod +x rke
Im nächsten Schritt wird mit Hilfe des Tools die Konfiguration des Clusters definiert:
./rke config
[+] Cluster Level SSH Private Key Path [~/.ssh/id_rsa]:
[+] Number of Hosts [1]: 3
[+] SSH Address of host (1) [none]: node-1.example.org
[+] SSH Port of host (1) [22]:
[+] SSH Private Key Path of host (node-1.example.org) [none]:
[-] You have entered empty SSH key path, trying fetch from SSH key parameter
[+] SSH Private Key of host (node-1.example.org) [none]:
[-] You have entered empty SSH key, defaulting to cluster level SSH key: ~/.ssh/id_rsa
[+] SSH User of host (node-1.example.org) [ubuntu]:
[+] Is host (node-1.example.org) a Control Plane host (y/n)? [y]:
[+] Is host (node-1.example.org) a Worker host (y/n)? [n]: y
[+] Is host (node-1.example.org) an etcd host (y/n)? [n]: y
[+] Override Hostname of host (node-1.example.org) [none]:
[+] Internal IP of host (node-1.example.org) [none]: 192.168.0.1
[+] Docker socket path on host (node-1.example.org) [/var/run/docker.sock]:
[+] SSH Address of host (2) [none]: node-2.example.org
[+] SSH Port of host (2) [22]:
[+] SSH Private Key Path of host (node-2.example.org) [none]:
[-] You have entered empty SSH key path, trying fetch from SSH key parameter
[+] SSH Private Key of host (node-2.example.org) [none]:
[-] You have entered empty SSH key, defaulting to cluster level SSH key: ~/.ssh/id_rsa
[+] SSH User of host (node-2.example.org) [ubuntu]:
[+] Is host (node-2.example.org) a Control Plane host (y/n)? [y]:
[+] Is host (node-2.example.org) a Worker host (y/n)? [n]: y
[+] Is host (node-2.example.org) an etcd host (y/n)? [n]: y
[+] Override Hostname of host (node-2.example.org) [none]:
[+] Internal IP of host (node-2.example.org) [none]: 192.168.0.2
[+] Docker socket path on host (node-2.example.org) [/var/run/docker.sock]:
[+] SSH Address of host (3) [none]: node-3.example.org
[+] SSH Port of host (3) [22]:
[+] SSH Private Key Path of host (node-3.example.org) [none]:
[-] You have entered empty SSH key path, trying fetch from SSH key parameter
[+] SSH Private Key of host (node-3.example.org) [none]:
[-] You have entered empty SSH key, defaulting to cluster level SSH key: ~/.ssh/id_rsa
[+] SSH User of host (node-3.example.org) [ubuntu]:
[+] Is host (node-3.example.org) a Control Plane host (y/n)? [y]:
[+] Is host (node-3.example.org) a Worker host (y/n)? [n]: y
[+] Is host (node-3.example.org) an etcd host (y/n)? [n]: y
[+] Override Hostname of host (node-3.example.org) [none]:
[+] Internal IP of host (node-3.example.org) [none]: 192.168.0.3
[+] Docker socket path on host (node-3.example.org) [/var/run/docker.sock]:
[+] Network Plugin Type (flannel, calico, weave, canal) [canal]:
[+] Authentication Strategy [x509]:
[+] Authorization Mode (rbac, none) [rbac]:
[+] Kubernetes Docker image [rancher/hyperkube:v1.13.5-rancher1]:
[+] Cluster domain [cluster.local]:
[+] Service Cluster IP Range [10.43.0.0/16]:
[+] Enable PodSecurityPolicy [n]:
[+] Cluster Network CIDR [10.42.0.0/16]:
[+] Cluster DNS Service IP [10.43.0.10]:
[+] Add addon manifest URLs or YAML files [no]:
Das Ergebnis ist eine YAML-Datei cluster.yml
, die unseren Cluster beschreibt:
nodes:
- address: node-1.example.org
port: "22"
internal_address: 192.168.0.1
role:
- controlplane
- worker
- etcd
hostname_override: ""
user: ubuntu
docker_socket: /var/run/docker.sock
ssh_key: ""
ssh_key_path: ~/.ssh/id_rsa
labels: {}
- address: node-2.example.org
port: "22"
internal_address: 192.168.0.2
role:
- controlplane
- worker
- etcd
hostname_override: ""
user: ubuntu
docker_socket: /var/run/docker.sock
ssh_key: ""
ssh_key_path: ~/.ssh/id_rsa
labels: {}
- address: node-3.example.org
port: "22"
internal_address: 192.168.0.3
role:
- controlplane
- worker
- etcd
hostname_override: ""
user: ubuntu
docker_socket: /var/run/docker.sock
ssh_key: ""
ssh_key_path: ~/.ssh/id_rsa
labels: {}
services:
etcd:
image: ""
extra_args: {}
extra_binds: []
extra_env: []
external_urls: []
ca_cert: ""
cert: ""
key: ""
path: ""
snapshot: null
retention: ""
creation: ""
kube-api:
image: ""
extra_args: {}
extra_binds: []
extra_env: []
service_cluster_ip_range: 10.43.0.0/16
service_node_port_range: ""
pod_security_policy: false
kube-controller:
image: ""
extra_args: {}
extra_binds: []
extra_env: []
cluster_cidr: 10.42.0.0/16
service_cluster_ip_range: 10.43.0.0/16
scheduler:
image: ""
extra_args: {}
extra_binds: []
extra_env: []
kubelet:
image: ""
extra_args: {}
extra_binds: []
extra_env: []
cluster_domain: cluster.local
infra_container_image: ""
cluster_dns_server: 10.43.0.10
fail_swap_on: false
kubeproxy:
image: ""
extra_args: {}
extra_binds: []
extra_env: []
network:
plugin: canal
options: {}
authentication:
strategy: x509
options: {}
sans: []
addons: ""
addons_include: []
system_images:
etcd: rancher/coreos-etcd:v3.2.24
alpine: rancher/rke-tools:v0.1.16
nginx_proxy: rancher/rke-tools:v0.1.16
cert_downloader: rancher/rke-tools:v0.1.16
kubernetes_services_sidecar: rancher/rke-tools:v0.1.16
kubedns: rancher/k8s-dns-kube-dns-amd64:1.15.0
dnsmasq: rancher/k8s-dns-dnsmasq-nanny-amd64:1.15.0
kubedns_sidecar: rancher/k8s-dns-sidecar-amd64:1.15.0
kubedns_autoscaler: rancher/cluster-proportional-autoscaler-amd64:1.0.0
kubernetes: rancher/hyperkube:v1.13.5-rancher1
flannel: rancher/coreos-flannel:v0.10.0
flannel_cni: rancher/coreos-flannel-cni:v0.3.0
calico_node: rancher/calico-node:v3.4.0
calico_cni: rancher/calico-cni:v3.4.0
calico_controllers: ""
calico_ctl: rancher/calico-ctl:v2.0.0
canal_node: rancher/calico-node:v3.4.0
canal_cni: rancher/calico-cni:v3.4.0
canal_flannel: rancher/coreos-flannel:v0.10.0
wave_node: weaveworks/weave-kube:2.5.0
weave_cni: weaveworks/weave-npc:2.5.0
pod_infra_container: rancher/pause-amd64:3.1
ingress: rancher/nginx-ingress-controller:0.21.0-rancher1
ingress_backend: rancher/nginx-ingress-controller-defaultbackend:1.4
metrics_server: rancher/metrics-server-amd64:v0.3.1
ssh_key_path: ~/.ssh/id_rsa
ssh_agent_auth: false
authorization:
mode: rbac
options: {}
ignore_docker_version: false
kubernetes_version: ""
private_registries: []
ingress:
provider: ""
options: {}
node_selector: {}
extra_args: {}
cluster_name: ""
cloud_provider:
name: ""
prefix_path: ""
addon_job_timeout: 0
bastion_host:
address: ""
port: ""
user: ""
ssh_key: ""
ssh_key_path: ""
monitoring:
provider: ""
options: {}
Mit folgendem Befehl wird der Cluster auf den vorbereiteten 3 Servern ausgerollt:
./rke up
Das Ergebnis dieses Befehls ist auch eine Kubeconfig-Datei, die wir in jedem folgendem kubectl-Befehl mit angeben oder als ~/.kube/config
speichern:
mv kube_config_cluster.yml ~/.kube/config
Der Cluster sollte nach ein paar Minuten aufgesetzt sein. Bei der Überprüfung der laufenden Pods:
kubectl get po --all-namespaces
sollten alle Pods den Status "Running" oder "Completed" haben.
Helm
Die Client-Komponente des Helm wird auf dem lokalen Rechner installiert:
curl https://raw.githubusercontent.com/helm/helm/master/scripts/get > get_helm.sh
chmod 700 get_helm.sh
./get_helm.sh
Die Server-Komponente von Helm, der Helm-Tiller wird im Cluster installiert. Zuerst muss aber noch ein Service-Account angelegt und mit der Role "cluster-admin" verbunden werden.
kubectl -n kube-system create serviceaccount tiller
kubectl create clusterrolebinding tiller \
--clusterrole=cluster-admin \
--serviceaccount=kube-system:tiller
Die Installation des Tiller:
helm init --service-account tiller
Cert-Manager
Um TLS-Zertifikate der in dem Cluster veröffentlichter Anwendungen einfach anlegen zu können und automatisiert verlängern zu lassen, wird "Cert-Manager" installiert.
"Cert-Manager" ist ein natives Kubernetes-Zertifikatsverwaltungstool. Es unterstützt bei der Ausstellung und Verwaltun von Zertifikaten aus einer Vielzahl von Quellen, z.B. Let´s Encrypt, HashiCorp Vault, etc.
Installiert werden kann das Tool auch mit Helm.
Zuerst wird eine CustomResourceDefinition installiert:
kubectl apply -f https://raw.githubusercontent.com/jetstack/cert-manager/release-0.7/deploy/manifests/00-crds.yaml
Und ein Namespace angelegt:
kubectl create namespace cert-manager
Für das Cert-Manager-Namespace wird die Ressourcen-Validierung deaktiviert:
kubectl label namespace cert-manager certmanager.k8s.io/disable-validation=true
Jetstack-Helm-Repository hinzufügen:
helm repo add jetstack https://charts.jetstack.io
helm repo update
Der Cert-Manager kann jetzt installiert werden:
helm install \
--name cert-manager \
--namespace cert-manager \
--version v0.7.0 \
jetstack/cert-manager
Wie man mit Cert-Manager Let's Encrypt Zertifikate anlegt habe ich bereits in einem Beitrag beschrieben. Für einen Funktionstest kann ein Beispiel von dort verwendet werden.
Logging
Für die Log-Verwaltung kann zum Beispiel ein ELK-Stack eingesetzt werden. Ein mögliches Setup habe ich hier beschrieben. Im Kubernetes-Cluster muss lediglich Filebeat installiert werden um Logs an Elasticsearch / Logstash weiter zu leiten. Hierfür kann folgende Konfiguration verwendet werden (filebeat.yml
):
---
apiVersion: v1
kind: ConfigMap
metadata:
name: filebeat-config
namespace: kube-system
labels:
k8s-app: filebeat
kubernetes.io/cluster-service: "true"
data:
filebeat.yml: |-
filebeat.registry_file: /var/log/filebeat.registry
logging.level: error
filebeat.inputs:
- type: log
paths:
- /var/lib/docker/containers/*/*.log
symlinks: true
json.keys_under_root: true
json.message_key: log
processors:
- add_kubernetes_metadata:
in_cluster: true
output.logstash:
hosts: [ $LOGSTASHIP ]
---
apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
name: filebeat
namespace: kube-system
labels:
k8s-app: filebeat
kubernetes.io/cluster-service: "true"
spec:
template:
metadata:
labels:
k8s-app: filebeat
kubernetes.io/cluster-service: "true"
spec:
serviceAccountName: filebeat
terminationGracePeriodSeconds: 30
containers:
- name: filebeat
image: docker.elastic.co/beats/filebeat:6.6.2
imagePullPolicy: Always
args: [
"-c", "/etc/filebeat.yml",
"-e",
]
securityContext:
runAsUser: 0
volumeMounts:
- name: config
mountPath: /etc/filebeat.yml
readOnly: true
subPath: filebeat.yml
- name: data
mountPath: /usr/share/filebeat/data
- name: varlibdockercontainers
mountPath: /var/lib/docker/containers
readOnly: true
- name: dockersock
mountPath: /var/run/docker.sock
volumes:
- name: config
configMap:
name: filebeat-config
- name: data
emptyDir: {}
- name: varlibdockercontainers
hostPath:
path: /var/lib/docker/containers
- name: dockersock
hostPath:
path: /var/run/docker.sock
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: filebeat
subjects:
- kind: ServiceAccount
name: filebeat
namespace: kube-system
roleRef:
kind: ClusterRole
name: filebeat
apiGroup: rbac.authorization.k8s.io
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: filebeat
labels:
k8s-app: filebeat
rules:
- apiGroups: [""]
resources:
- namespaces
- pods
verbs:
- get
- watch
- list
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: filebeat
namespace: kube-system
labels:
k8s-app: filebeat
Mit folgendem Befehl kann im obigen YAML-File die IP-Adresse des Logstash gesetzt und die Konfiguration angewendet werden:
cat filebeat.yml |
sed 's~\$LOGSTASHIP'"~192.168.200.4:5035~g" |
rancher kubectl apply -f -
Im Kibana sollten nun Logs des Clusters auftauchen.
Festspeicher
Eine wichtige Fähigkeit von Kubernetes ist das "dynamic volume provisioning". Darauf sollte man in einem Cluster nicht verzichten. Die im nächsten Abschnitt beschriebene Monitoring-Lösung benötigt übrigens auch dynamische Zuweisung von Volumes.
Wer seinen Cluster auf eigener Hardware betreibt, kann das dynamische Provisioning mit einem selbst gehosteten verteilten Dateisystem wie GlusterFS gewährleistet werden.
"gluster-kubernetes" ist ein Projekt, das Kubernetes-Administratoren einen Mechanismus zur einfachen Bereitstellung von GlusterFS als nativen Speicherdienst in einem vorhandenen Kubernetes-Cluster zur Verfügung stellt.
Auf jeder Cluster-Node müssen zusätzliche Kernel-Module aktiviert werden:
sudo -i
echo -e "\ndm_snapshot\ndm_mirror\ndm_thin_pool" >> /etc/modules
modprobe dm_snapshot && modprobe dm_mirror && modprobe dm_thin_pool
exit
Und Gluster-Client muss installiert werden:
sudo add-apt-repository ppa:gluster/glusterfs-3.10
sudo apt install glusterfs-client -y
Falls keine Partition, die dediziert für GlusterFS verwendet werden könnte existiert, dann kann ein Loop-Device angelegt werden:
sudo dd if=/dev/zero of=/glusterimage bs=1M count=102400
sudo losetup /dev/loop0 /glusterimage
Damit das Loopback-Device nach jedem Neustart der Node angelegt wird, wird ein Systemd-Service angemeldet. Die Datei /etc/systemd/system/losetup-gluster.service
mit folgendem Inhalt wird angelegt:
[Unit]
Description=Activate loop device
DefaultDependencies=no
After=systemd-udev-settle.service
Before=lvm2-activation-early.service
Wants=systemd-udev-settle.service
[Service]
ExecStart=/sbin/losetup /dev/loop0 /glusterimage
Type=oneshot
[Install]
WantedBy=local-fs.target
Der Service wird registriert:
sudo systemctl enable /etc/systemd/system/losetup-gluster.service
Jetzt kann der Code des Gluster-Installers ausgecheckt werden.
git clone git@github.com:gluster/gluster-kubernetes.git
Bevor GlusterFS installiert werden kann müssen die vorbereiteten "Laufwerke" in einer JSON-Datei dokumentiert werden (gluster-cluster.json):
{
"clusters": [
{
"nodes": [
{
"node": {
"hostnames": {
"manage": ["node-1"],
"storage": ["192.168.0.1"]
},
"zone": 1
},
"devices": ["/dev/loop0"]
},
{
"node": {
"hostnames": {
"manage": ["node-2"],
"storage": ["192.168.0.2"]
},
"zone": 1
},
"devices": ["/dev/loop0"]
},
{
"node": {
"hostnames": {
"manage": ["node-3"],
"storage": ["192.168.0.3"]
},
"zone": 1
},
"devices": ["/dev/loop0"]
}
]
}
]
}
Alle Nodes müssen untereinander per SSH erreichbar sein und die Legitimierung per Public-Key muss konfiguriert sein.
Nun kann GlusterFS installiert werden:
./gk-deploy -gy gluster-cluster.json
Eine StorageClass kann nun angelegt werden (Die IP der Node, auf der Heketi deploed ist wurde zuvor ermittelt mit kubectl get svc/heketi --template 'http://{{.spec.clusterIP}}:{{(index .spec.ports 0).port}}'
)
---
apiVersion: storage.k8s.io/v1beta1
kind: StorageClass
metadata:
name: gluster-heketi
provisioner: kubernetes.io/glusterfs
parameters:
resturl: "http://192.168.10.2:40525"
Wenn die GlusterFS-StorageClass die Default-StorageClass sein soll, kann sie noch entsprechend markiert werden:
kubectl patch storageclass gluster-heketi -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
Ab jetzt können PersistentVolumeClaims dynamisch angelegt und verwendet werden.
Monitoring, Alerting
Fürs Monitoring und Alerting kann "Prometheus-Operator" als Helm-Chart installiert werden. Es ist einfach zu konfigurieren, bietet eine Vielzahl an vordefinierten Einstellungen und bringt Grafana und Node-Exporter mit. In wenigen Schritten ist das Monitoring des Clusters konfiguriert.
Prometheus-Operator installieren
Die nötigsten Einstellungen werden in einer Datei (z.B: prom.yml
) gespeichert:
coreDns:
enabled: false
kubeDns:
enabled: true
alertmanager:
alertmanagerSpec:
storageSpec:
volumeClaimTemplate:
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: gluster-heketi
resources:
requests:
storage: 5Gi
prometheus:
prometheusSpec:
storageSpec:
volumeClaimTemplate:
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: gluster-heketi
resources:
requests:
storage: 5Gi
grafana:
adminPassword: "yourpass"
persistence:
enabled: true
accessModes: ["ReadWriteOnce"]
size: 5Gi
Die Installation im Namespace "monitoring"
helm install \
--name prom \
--namespace monitoring \
-f prom.yaml \
stable/prometheus-operator
Grafana aufrufen
Sobald die Installation erfolgreich abgeschlossen ist, können die gesammelten Monitoring-daten aubgerufen werden. Mittels kubectl proxy
kann auf Grafana zugegriffen werden. Mit folgendem Befehl wird eine Portweiterleitung zwischen dem Grafana-Pod und dem lokalen Rechner eingerichtet:
kubect-n monitoring port-forward $(kubectl get po -n monitoring | grep grafana | awk '{ print $1 }') 3000
Nach Login (mit Zugangsdaten aus prom.yml) können bereits einige Dashboars begutachtet werden:
Alertmanager aufrufen
Die grafische Oberfläche des Alertmanager kann analog zum obigen Grafana-Beispiel über Kubernetes-Proxy hergestellt werden:
kubectl -n monitoring port-forward $(kubectl get po -n monitoring | grep alertmanager | awk '{ print $1 }') 9093
Es gibt bereits vier voreingestellte Alerts:
Alerts versenden
Um Alerts zum Beispiel per E-Mail zu versenden, muss die Konfiguration des Alertmanager in der obigen prom.yml
erweitert werden.
Im folgenden Beispiel werden globale SMTP-Einstellungen, eine Default-Route für alle Alerts, sowie eine Route, die kritische Meldungen behandelt.
alertmanager:
alertmanagerSpec:
storageSpec:
volumeClaimTemplate:
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: gluster-heketi
resources:
requests:
storage: 5Gi
config:
global:
smtp_smarthost: "smtp.example.org:587"
smtp_hello: smtp.example.org
smtp_require_tls: true
smtp_from: alertmanager@example.org
smtp_auth_username: username
smtp_auth_password: password
route:
# default route if none match
group_wait: 30s
group_interval: 5m
repeat_interval: 2h
receiver: email-fallback-receiver
group_by: ["alertname"]
routes:
# send critical messages to email-receiver
- receiver: email-critical-receiver
match:
severity: critical
receivers:
- name: email-fallback-receiver
email_configs:
- to: warning@example.org
- name: email-critical-receiver
email_configs:
- to: critical@example.org
Die aktualisierte Definition kann dann veröffentlicht werden:
helm upgrade prom --namespace monitoring -f prometheus-operator-values.yml stable/prometheus-operator
Fazit
Nach etwas Einarbeitung, ist der Betrieb eines eigenen Kubernetes-Clusters keine Horror-Vorstellung. Im Gegenteil: das Setup kann flexibler und vor allem viel günstiger sein, als dessen gemanagte alternativen.
https://rancher.com/docs/rancher/v2.x/
https://helm.sh/docs/using_helm/
https://docs.cert-manager.io/en/latest/getting-started/install.html
https://www.elastic.co/guide/en/beats/filebeat/master/running-on-kubernetes.html
https://kubernetes.io/blog/2018/04/13/local-persistent-volumes-beta/
https://github.com/helm/charts/tree/master/stable/prometheus-operator