/ Server

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:
prometheus-operator-grafana-dashboards

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:
alertmanager-default-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