Kubernetes Cluster mit Rancher und Ansible

Puzzle und Red Hat OpenShift gehen schon lange gemeinsame Wege. Auch mit APPUiO setzt Puzzle zusammen mit VSHN auf Red Hat OpenShift. Warum interessiert nun Kubernetes? OpenShift ist eine Kubernetes Distribution und daher sammeln wir auch Erfahrungen mit „Vanilla Kubernetes“ (back to the roots). In diesem Beitrag zeigt Sebastian Plattner, wie mit Hilfe von Rancher und Ansible ein Kubernetes Cluster erstellt und betrieben werden kann.

Was sind eigentlich die Unterschiede zwischen OpenShift und Kubernetes? Im Vergleich zu Vanilla Kubernetes hat Red Hat OpenShift Kubernetes Distribution folgende, wesentliche Unterschiede:

  • Professional Support von Red Hat
  • BuiltIn Monitoring & Logging
  • BuiltIn Container Registry
  • BuiltIn Pipeline und Image Builder
  • Vorgefertigte Security Policies

Auf weitere Details gehe ich hier nicht ein. Wer mehr Informationen dazu will, kann sich zum Beispiel den Beitrag von Red Hat zu Enterprise Kubernetes anschauen.

Rancher

Rancher ist eine (OpenSource) Kubernetes Management & Orchestration Platform, die es ermöglicht, einen Kubernetes Cluster aufzubauen und zu betreiben. Mit Rancher wird aber nicht nur ein Kubernetes Cluster verwaltet. Es werden mehrere, auch auf unterschiedlichen (Cloud-)Plattformen oder on Premises laufende Cluster in einem zentralen web-basierten UI administriert. Die mit Rancher installierten Kubernetes Cluster entsprechen dabei der native upstream Kubernetes Version.

Rancher

Aus persönlicher Erfahrung mit Rancher, aber auch durch die Erfahrungen die ich bei der Mobiliar machen durfte, habe ich für Puzzle ebenfalls Rancher installiert und damit einen Kubernetes Cluster gebaut. Der Cluster läuft natürlich nicht einfach nur im Leerlauf, mittlerweile laufen diverse unserer internen Applikationen darauf (als Alternative zu unserer internen OpenShift Plattform).

Im Zuge dieses Efforts sind auch, wie von Nik Wolfgramm in seinem Blog-Post bereits erwähnt, zwei OpenSource Contributions zu Rancher und unserem Cloudprovider cloudscale.ch entstanden. Ich habe einen Rancher-UI Treiber zum automatisch Provisionieren von VM’s und anschliessendem deployen eines Kubernetes Cluster auf cloudscale.ch erstellt. Die beiden Projekte sind nun in der Obhut von cloudscale.ch.

Ansible Rollen

Puzzle verwendet Ansible, um die eigene Infrastruktur auf cloudscale.ch zu betreiben. Daher habe auch ich Ansible-Rollen für Rancher und die Kubernetes Plattform geschrieben. Für eine High-Availability Installation setzt Rancher auf einen Kubernetes Cluster und nutzt dessen Vorteile in der Container Orchestrierung. Mit Hilfe von Rancher Kubernetes Engine, einem Kubernetes Installer, wird zuerst ein 3-Node Kubernetes Cluster aufgebaut. Anschliessend wird dann mit Helm Rancher installiert. Sobald der Rancher Cluster steht, können damit weitere Kubernetes Cluster gebaut werden. Es gibt bereits Integrationen, um auf AWS, Azure, GoogleCloudPlattform etc. Cluster zu erstellen oder es kann – wie oben erwähnt –  beispielsweise die cloudscale.ch-Integration verwendet werden. Weiter können aber auch selbst provisionierte VM’s genutzt werden, um Kubernetes zu deployen. Da wir bereits etablierte Rollen für unser Base-VM Setup haben, wähle ich diesen Weg, um den Kubernetes Cluster zu bauen. Ich habe zwei Ansible Rollen erstellt: Eine, um einen Kubernetes-Cluster mit RKE und anschliessender Installation von Rancher zu erstellen. Die zweite Rolle dient dazu, mit einer bestehenden Rancher Installation, einen neuen Kubernetes Cluster zu bauen.

Zuerst ein paar Details zur rke_rancher_cluster-Rolle. Die Rolle wird auf einen Ansible-Host angewendet. Dieser Ansible-Host ist nicht wie sonst üblich eine VM (oder sonst ein Device auf das per SSH zugegriffen werden kann), sondern repräsentiert den Cluster. Zu diesem Cluster-Ansible-Host gehört dann eine Ansible-Gruppe, welche die effektiven Cluster-Members (=VMs) enthält.

[rke_rancher_clusters]
cluster_rancher_ha # Belongs to Ansible Group rke_cluster_rancher_ha

[rke_cluster_rancher_ha]
rancher-node01
rancher-node02
rancher-node03

Über die Host-Variabeln des Cluster-Host (bzw. auch die defaults der Rolle), bestimme ich z.B. die Rancher-Version, die installiert werden soll:

rancher_version: v2.2.6

Auf die VMs in der rke_cluster_rancher_ha-Gruppe wird die bestehende Base-Rolle inkl. VM-Provisioning angewendet. Auf die Member der rke_rancher_clusters Ansible-Gruppe dann die rke_rancher_cluster-Rolle. Da cluster_rancher_ha nicht wirklich ein Host ist, muss natürlich gather_facts: no und auch ein delegate_to: localhost gesetzt werden. Die rke_rancher_cluster-Rolle macht dann folgendes:

1. Herunterladen von benötigten Binaries wie rke, kubectl und helm.

2. Erstellen der Cluster-Konfiguration für RKE. Dies wird mit Hilfe derMember der rke_cluster_rancher_ha-Ansible-Gruppe gemacht.

---
- name: Create RKE Config
  template:
    src: cluster.yml.j2
    dest: "{{ rke_cluster_config }}"
    mode: 0666
  vars:
    cluster: "{{ groups['rke_' + inventory_hostname] }}"
  changed_when: false

Man beachte, in inventory_hostname steht der Cluster-Name, sprich der erwähnte Cluster-Host. Da Gruppen und Hosts nicht gleich heissen sollten, verwende ich hier ein rke_ Prefix.

Im Jinja-Template kann dann auf die Nodes im Cluster zugegriffen werden, um die Konfiguration für rke zu erstellen:

---
[...]
nodes:
{% for node in cluster %}
  - address: {{ hostvars[node]['ansible_eth0']['ipv4']['address'] }}
    internal_address: {{ hostvars[node]['ansible_eth1']['ipv4']['address'] }}
    user: sshdocker
    role: [controlplane,worker,etcd]
    ssh_key_path: {{local_cache_dir }}/ansible_ssh/id_rsa
{% endfor %}
[...]

3. Mit dieser Cluster-Konfiguration wird dann ein rke up –config cluster.yml ausgeführt, das den Kubernetes Cluster auf den drei VMs erstellt. RKE verwendet ein StateFile um den aktuellen Zustand des Clusters zu speichern. Somit ist dieser Schritt idempotent und kann daher immer wieder ausgeführt werden. Das Statefile speichere ich in einem S3 Bucket. Nach dem rke up ist auch ein kube.config vorhanden, um mit Hilfe von kubectl mit dem Kubernetes Cluster zu agieren.

4. Nun folgend diverse Post-Install Tasks, die z.B. VIP-Adressen für das HA-Setup im Kubernetes Cluster Installieren (mit Hilfe von keepalived und einem DaemonSet). Nur mit den VIP-Adressen ist Rancher wirklich HA und unter diesen VIP Adressen bzw. einem entsprechenden DNS-Eintrag erreichbar.

5. Im letzten Schritt wird dann mit Hilfe von Helm und den Rancher Helm Charts Rancher auf dem Cluster installiert. Sobald der Rancher Cluster ready ist, wird zum Schluss noch das initiale Admin-Passwort gesetzt (sofern es ein neuer Cluster ist).

Nun ist Rancher ready und kann verwendet werden. Naja fast 😉 , die initiale Konfiguration der Authentifizierung an Rancher habe ich z.B. leider noch nicht automatisiert. Wir verwenden hier Keycloack und SAML.

Die zweite Rolle custom_rancher_cluster (custom, da diese Variante in Rancher halt so heisst) installiert dann einen Kubernetes Cluster mit Hilfe des vorhandenen Rancher Cluster. Ganz ähnlich wie zuvor, verwende ich auch hier wieder einen Ansible-Host, der den Cluster repräsentiert. Dazu gehören nun aber drei Ansible-Gruppen, die jeweils die Rolle der einzelnen VM (=Kubernetes Nodes) bestimmen. In der Master-Gruppe sind z.B. die Nodes, die anschliessend die Master-Rolle übernehmen (api-server, etcd etc). Ingress Nodes haben ein public und private Network Interface, worauf der Ingress Controller laufen wird. Worker Nodes sind normale Kubernetes Nodes für Container Workload.

[custom_rancher_clusters]
rancher_k8s_prod_cluster # Belongs to Ansible Group k8s-prod-cluster

[k8s_prod_cluster:children]
k8s_prod_cluster_master
k8s_prod_cluster_worker
k8s_prod_cluster_ingress

[k8s_prod_cluster_master]
k8s-master01
k8s-master02
k8s-master03

[k8s_prod_cluster_worker]
k8s-worker01

[k8s_prod_cluster_ingress]
k8s-ingress01
k8s-ingress02

Über die Host Variabeln des Cluster Host (bzw. auch die defaults der Rolle), bestimme ich z.B. die Kubernetes Version, die verwendet werden soll:

# Kubernetes Version
custom_rancher_cluster_kubernetes_version: v1.13.5-rancher1-2

Auf die VMs wird wiederum zuerst die Base-Rolle angewendet und auf dem Host in den custom_rancher_clusters die custom_rancher_cluster-Rolle. In der Rolle geschieht folgendes:

1. Zuerst wird geschaut, ob Rancher den gewünschten Cluster schon kennt. Dieser wird wenn nötig über die Rancher-API erstellt. Anschliessend werden entsprechende API-Tokens sowie kube.config-Files via Rancher-API abgefragt. Diese Daten werden benötigt, um dem Cluster Nodes hinzuzufügen oder zur Interaktion mit dem Kubernetes Cluster bei Post-Install Operations.

2. Nun werden die Nodes hinzugefügt (sofern diese nicht schon im Kubernetes Cluster vorhanden sind). Dazu muss auf der VM ein Docker Container mit dem Rancher Agent gestartet werden. Dieser übernimmt dann die Kommunikation zu Rancher und installiert alles was nötig ist auf dem Node.

- name: Add Master Nodes when not already added
  delegate_to: "{{ item }}"
  command: "{{ custom_rancher_cluster_docker_commmand_base }} 
    --token {{ clusterregistrationtoken }} 
    --etcd --controlplane --worker 
    --label vip_private=true"
  when:
    - (cluster_nodes | json_query("json.data[?hostname == '" + hostvars[item]['ansible_facts']['hostname'] + "']") | length) == 0
  with_items:
    - "{{ groups[custom_rancher_cluster_group_inventory_name + '_master'] }}"

- name: Add Worker Nodes when not already added
  delegate_to: "{{ item }}"
  command: "{{ custom_rancher_cluster_docker_commmand_base }} 
    --token {{ clusterregistrationtoken }} 
    --worker"
  when:
    - (cluster_nodes | json_query("json.data[?hostname == '" + hostvars[item]['ansible_facts']['hostname'] + "']") | length) == 0
  with_items:
    - "{{ groups[custom_rancher_cluster_group_inventory_name + '_worker'] }}"

- name: Add Public Ingress Nodes when not already added
  delegate_to: "{{ item }}"
  command: "{{ custom_rancher_cluster_docker_commmand_base }} 
    --token {{ clusterregistrationtoken }} 
    --worker 
    --internal-address {{ custom_rancher_cluster_ingress_node_internal_iface }} 
    --label vip_public=true"
  when:
    - (cluster_nodes | json_query("json.data[?hostname == '" + hostvars[item]['ansible_facts']['hostname'] + "']") | length) == 0
  with_items:
    - "{{ groups[custom_rancher_cluster_group_inventory_name + '_ingress'] }}"

3. In einem Post-Install Task installiere ich anschliessend wiederum z.B. VIP-Adressen für die Ingress-Controller.

Neue Members können durch eintragen von Hosts in die entsprechende Ansible-Gruppe zum Kubernetes Cluster hinzugefügt werden.

Damit der Cluster wirklich Production ready ist, benötigt es noch ein paar weitere Schritte. So verwenden wir z.B.

Die Installation von diesen Komponenten erfolgt zur Zeit über den Rancher App Catalog oder manuell direkt mit kubectl. Auch dieser Schritt kann aber mit Ansible und der Rancher-API automatisiert werden.

3 Kommentare

  • Sebastian, 19. Oktober 2019

    Hallo. Toller Beitrag. Gibt es die ansible Scripts irgendwo auf github?

  • mm
    Sebastian Plattner, 21. Oktober 2019

    Vielen Dank für deinen Kommentar. Zur Zeit sind die Scripte leider noch nicht auf Github verfügbar, da sie noch stark in unsere bestehenden Ansible Playbooks und Rollen integriert sind. Wenn ich Zeit finde, werde ich sie gerne noch etwas überarbeiten und dann auf Github zur Wiederverwendung veröffentlichen.

    Grüsse, sebastian

  • mm
    Sebastian Plattner, 4. August 2020

    Unser Rancher Ansible Roles und Playbooks sind nun auf Github zu finden.