Rancher: das “gut & günstig” Setup mit K3s und Ansible
Rancher: das “gut & günstig” Setup mit K3s
Rancher ist ein tolles Tool, um mehrere Kubernetes Cluster schnell und einfach verwalten zu können. Bei trieb.work betreiben wir viele Services in Kubernetes Cluster, die wir mit der Rancher Oberfläche verwalten. Da Rancher selber in einem Kubernetes Cluster betrieben werden muss, hat man hier oft hohe laufende Kosten. Wir stellen hier im Kurzen unser Setup mit “K3s” statt “k8s” vor und wieso dieses Setup günstiger ist.
Unterschied zwischen K3s und k8s
Im Vergleich zu Kubernetes bietet K3s einen geringeren Funktionsumfang, ist jedoch schneller und einfacher zu installieren und zu verwalten. Es ist für Edge-Computing-Szenarien und Umgebungen mit geringen Ressourcen geeignet, während Kubernetes für größere, komplexere Deployments entwickelt wurde.
Da das Cluster nur Rancher als Applikation beheimatet, werden viele Features von Kubernetes nicht benötigt - es kann im allgemeinen einfacher aufgebaut sein und damit weniger Ressourcen verbrauchen.
K3s Aufbau in der Hetzner Cloud
Hetzner hat sich auch bei diesem Projekt als Hostinganbieter angeboten. Wir nutzen hier für einen simplen Aufbau zwei K3s VMs und eine PostgresQL VM als Datenspeicher.
Das “Bestellen” der VMs und des virtuellen Netz läuft komplett mittels Ansible. Der generelle Ablauf und die Strukturierung der Ansible Skripte sieht komplett so aus:
# SSH-Key einrichten
- hosts: localhost
connection: local
gather_facts: False
user: root
vars:
tasks:
- include_tasks: roles/hetzner-project/security.yml
# VMs bereitstellen (2 oder mehr Controllnodes,
# eine Datenbanknode)
- hosts: localhost
connection: local
gather_facts: False
user: root
vars:
datacenter_location: nbg1
tasks:
- include_tasks: roles/hetzner-server/hetzner-nodes-k3s.yml
# Internes netzwerk bereitstellen, alle Server hinzufügen
- hosts: localhost
user: root
connection: local
gather_facts: False
vars:
tasks:
- include_tasks: roles/hetzner-networking/hetzner-network.yml
# Komplettes Systemupgrade
- hosts: _managed_by_ansible_
user: root
tasks:
- include_tasks: roles/system-upgrade.yml
# Standard SSH-Hardening
- hosts: _managed_by_ansible_
user: root
gather_facts: False
collections:
- devsec.hardening
roles:
- ssh_hardening
vars:
ssh_permit_root_login: "without-password"
ssh_print_motd: true
ssh_allow_tcp_forwarding: 'yes'
# needed for ansible
sftp_enabled: true
# PostgresQL Service einrichten - traffic nur über internes
# Netz zulassen
- hosts: control-k3s-datastore
user: root
gather_facts: False
tasks:
- include_tasks: roles/postgresql/install.yml
- include_tasks: roles/postgresql/configure.yml
handlers:
- name: restart postgres
service: name=postgresql state=restarted
# k3s setup auf den Worker nodes ausführen
- hosts: _workload_type_control
user: root
gather_facts: False
tasks:
- include_tasks: roles/k3s/master.yml
# kubeconfig in der Konsole ausgeben. kann kopiert
# werden um von der lokalen Maschine aus k3s mit der kubectl
# anzusprechen
- hosts: control-k3s-1
user: root
gather_facts: False
name: Display content of k3s kubeconfig
tasks:
- name: Display k3s kubeconfig contents
command: cat /etc/rancher/k3s/k3s.yaml
register: command_output
- name: Print to console
debug:
msg: "{{ command_output.stdout_lines }} "
Die einzelnen Tasks werden jetzt im Detail beschrieben:
Hetzner Security Einstellungen setzen
Damit direkt der Zugriff mittels SSH auf die Nodes möglich ist, benötigen wir einen SSh-Key, der in das Hetzner Portal geladen werden kann. Wir verwenden hier ein “Operations” Keypair - alternativ sollte hier mit Zertifikaten gearbeitet werden.
Mit ansible laden wir den Public Key in das Hetzner Portal, um ihn später beim Erstellen von VMs auswählen zu können.
- name: Add the trieb.work engineers ssh-key
hetzner.hcloud.hcloud_ssh_key:
name: "trieb.work engineers"
public_key: "ecdsa-sha2-nistp521 AAAAE2VjZXXXXXXXXXX== info@tXXXXX"
state: present
Hetzner VMs bereitstellen
Wir benutzen ebenfalls das hetzner hcloud Plugin um die VM Server, die benötigt werden, automatisch bereitzustellen. Dabei kann man die Location auswählen. Wir legen ebenfalls eine Placementgruppe an, damit die VMs auf unterschiedlichen, physikalischen Servern bereitgestellt werden:
# roles/hetzner-server/hetzner-nodes-k3s.yml
- name: Create a basic placement group for worker nodes
hetzner.hcloud.hcloud_placement_group:
name: placement-group-worker-nodes
state: present
type: spread
- name: "Create k3s datastore node {{ datacenter_location }}"
hcloud_server:
name: control-k3s-datastore
server_type: cpx11
image: centos-stream-8
state: present
location: "{{ datacenter_location }}"
ssh_keys:
- "trieb.work engineers"
labels: {
workload_type: database,
managed_by_ansible: ""
}
- name: "Create k3s worker/control node {{ datacenter_location }}"
hetzner.hcloud.hcloud_server:
name: "control-k3s-{{ item }}"
server_type: cx31
image: centos-stream-8
state: present
location: "{{ datacenter_location }}"
ssh_keys:
- "trieb.work engineers"
labels: {
workload_type: control,
managed_by_ansible: ""
}
with_sequence: count=2
- name: Refresh inventory
meta: refresh_inventory
Der letzte Befehl “Refresh inventory” sorgt dafür, dass wir direkt auf die neu erstellten VMs zugreifen können und nicht Ansible erneut starten müssen.
Hetzner privates Netzwerk einrichten
Im nächsten Schritt legen wir ein privates Netzwek inklusive Subnet an, über welches die gesamte interne Kommunikation laufen soll. Dadurch müssen wir die Postgres Datenbank nicht im public Internet haben und können diesen Traffic über das virtuelle Hetzner Netz laufen lassen:
# roles/hetzner-networking/hetzner-network.yml
- name: Create the internal network
hetzner.hcloud.hcloud_network:
name: internal
ip_range: 10.0.0.0/8
state: present
- name: Create the internal subnetwork
hetzner.hcloud.hcloud_subnetwork:
network: internal
ip_range: 10.0.0.0/16
network_zone: eu-central
type: cloud
state: present
- name: Add all servers to network
hetzner.hcloud.hcloud_server_network:
server: "{{ item }}"
network: internal
with_inventory_hostnames:
- worker-*
- control-*
- etcd-*
register: internal_network
CentOS Systemupgrade
Ein komplettes System Upgrade sollte regelmäßig durchgeführt werden. Kernel Upgrades, die oftmals einen Systemneustart benötigen, lassen wir bei automatischen Upgrades allerdings aus. Die restlichen Pakete sollten kontinuierlich auf den neuesten Stand gebracht werden, um auch Security Patches zu installieren. Da fast alle Anwendungen in containerD laufen, ist die Abhängigkeit an bestimmte Betriebssystempaketversionen auch nicht so hoch. Bisher gab es mit diesem Setup noch keine Probleme.
- name: upgrade all packages, excluding kernel & containerd related packages
yum:
name: '*'
state: latest
exclude: kernel*,containerd*,docker*
SSH Hardening
Ein standard SSH-Hardening um Zugriffe einzuschränken. Dieser optionale Schritt härtet die SSH Konfiguration, damit der Zugriff nur mittels SSH Key funktioniert. Dafür wird das Open Source Playbook von “Dev-Sec” verwendet. In Zukunft soll eine alternative Zugriffsmöglichkeit gefunden werden, wie Ansible sich mit den Servern verbinden kann. Dedizierte IPv4 Adressen bedeuten zusätzliche Kosten bei Hetzner und am Ende auch einen zusätzlichen offenen Port im Internet.
Postgres Installation und Konfiguration
Als Datastore für k3s kommt eine dedizierte Postgres VM zum Einsatz. Das Setup mit Ansible ist sehr simpel und günstig. In unserem Setup verwenden wir eine CPX11-Instanz mit 2vCPUs und 2GB RAM. K3s hat hier eine sehr geringe Last auf das System und würde sogar mit einer noch kleineren Node auskommen.
Postgres muss nicht im Internet verfügbar gemacht werden, sondern ausschließlich über das private, interne Hetzner Netzwerk. Bei Hetzner VMs kann das Netzwerkinterface entweder “ens10” oder “enp7s0” benannt sein, weshalb wir die IP-Adresse von beiden Namen checken und als Variable abspeichern. Der Postgres Service wird dann entsprechend eingestellt, dass er nur auf der internen IP Adresse lauscht.
Die Install Routine sieht so aus:
# roles/postgresql/install.yml
- name: "Install packages"
dnf: "name={{ item }} state=present"
with_items:
- postgresql
- postgresql-server
- python3-psycopg2
- name: "Internal IP interfaces ens10"
set_fact:
internal_ip_postgres: "{{ ansible_facts.ens10.ipv4.address }}"
when: ansible_facts['ens10'] is defined
- name: "Internal IP interfaces enp7s0"
set_fact:
internal_ip_postgres: "{{ ansible_facts.enp7s0.ipv4.address }}"
when: ansible_facts['enp7s0'] is defined
- name: "Bootstrapping node with internal IP"
debug:
msg: "{{ internal_ip_postgres }}"
- name: "Make Postgres listen on internal address"
replace:
path: /var/lib/pgsql/data/postgresql.conf
regexp: '^#listen_addresses.*'
replace: "listen_addresses = '{{ internal_ip_postgres }}'"
notify: restart postgres
- name: "Find out if PostgreSQL is initialized"
ansible.builtin.stat:
path: "/var/lib/pgsql/data/pg_hba.conf"
register: postgres_data
- name: "Initialize PostgreSQL"
shell: "postgresql-setup initdb"
when: not postgres_data.stat.exists
- name: "Start and enable services"
service: "name={{ item }} state=started enabled=yes"
with_items:
- postgresql
Nachdem der Service an sich eingerichtet ist und läuft, können anschließend der Nutzer und die Datenbank für K3s eingerichtet werden:
# roles/postgresql/configure.yml
- name: "Create app database"
postgresql_db:
state: present
name: "{{ db_name }}"
become: yes
become_user: postgres
- name: "Create db user"
postgresql_user:
state: present
name: "{{ db_user }}"
password: "{{ db_password }}"
become: yes
become_user: postgres
- name: "Grant db user access to app db"
postgresql_privs:
type: database
database: "{{ db_name }}"
roles: "{{ db_user }}"
grant_option: no
privs: all
become: yes
become_user: postgres
- name: "Allow md5 connection for the db user"
postgresql_pg_hba:
dest: "~/data/pg_hba.conf"
contype: host
databases: all
method: md5
users: "{{ db_user }}"
create: true
address: "10.0.0.0/16"
become: yes
become_user: postgres
notify: restart postgres
K3s Installieren
Nachdem die Datenbank fertig eingerichtet worden ist, werden jetzt die K3s Nodes installiert und eingerichtet. Dabei wird prinzipiell nur das K3s Script auf der Node ausgeführt. Das Skript ist idempotent, wenn entsprechend die Version gepinned ist. Diese setzen wir mit einer Ansible Variable.
#roles/k3s/master.yml
- name: Start the k3s script
shell: "curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION={{ k3s_version }} K3S_DATASTORE_ENDPOINT='postgres://{{ db_user }}:{{ db_password }}@{{ hostvars['control-k3s-datastore']['internal_ip_postgres'] }}:5432/k3s?sslmode=disable' K3S_TOKEN={{ k3s_token }} sh -"
- Start des k3s setups auf den Worker-Nodes. Hierbei richten wir auch die beiden IP Adressen in Cloudflare im DNS ein. Die Controlnodes sind jetzt mit dem entsprechenden DNS Namen im Internet verfügbar. Wenn kein Cloudflare, oder ein anderer flexibler DNS Service besteht, kann dieser Schritt auch einfach manuell durchgeführt werden. Da diese Nodes stateless sind, können sie einfach ausgetauscht werden, womit sich im Normalfall auch die IP Adresse ändert.
- Nach dem Setup wird der Inhalt der kubeconfig auf der Console angezeigt. Dieses sollte kopiert werden und in die lokale Konfig der kubectl eingefügt werden