Kubernetes är ett open source-system skapat av Google för att hantera container-miljöer vad gäller skalning, deployment, operations samt uppgradering. När Apcera presenterade sina avsikter att inkludera stöd för Kubernetes så uppkom tanken hos Magnus Persson om att skapa ett litet Kubernetes-kluster hemmavid — allt för att kunna köra multiplayer-spelservrar för Qake, ett spel som utvecklas av skribenten.
Syftet med det här projektet var att skapa ett strömsnålt litet Docker-kluster som hanteras med Kubernetes. Ett kluster baserat på 4 stycken Raspberry Pi3 (Rpi3).
Klustret visade sig endast använda mellan 6-12W (c:a 0.1A) vid normal last. Lagom nivå när man vill ha ett eget kluster hemma!
Användningsområdet för klustret är att köra multiplayer-spelservrar för ett spel som är under utveckling av skribenten (http://qake.se). Spelservern är skriven i Javascript för både backend (NodeJS) och frontend (WebGL).
(En mycket tidigt demo utan multiplayerstöd finns att spela på: http://qake.se/demo/.)
Kubernetes går att köra på både publika och privata moln samt skapa hybridmiljöer. De containers som används i det här projektet är Docker-containers.
Mitt hemmaprojekt består av två delar:
Totalt ger de 4 RPi3 korten 4GB RAM samt 16 kärnor med 1.2GHz/kärna.
Mellan klient (webbläsare) och server används websockets. Spelservern emulerar hela spelvärlden på serversidan (”headlesss”) för att förhindra fusk med samma tekniker som används av stora spel som Half-Life och Quake.
All data som skickas mellan client/server är komprimerad och alla modeller läses in av servern som skickas i komprimerad binärdata till klienten. Alla rörelser som en klient gör emuleras även det på servern. Det innebär att spelservern har ett ganska krävande jobb och ju fler spelare desto fler serverinstanser krävs för att hålla upp i takt med antal spelare. Då passar ett autoskalande Kubernetes-kluster in riktigt bra.
Raspberry Pi bygger på en processorarkitektur som heter ARM:
Den skiljer sig åt från exempelvis x86 plattformen vilket de flesta datorerna har idag. Det medför att program kompilerade för x86 inte går att köra på ARM och då heller inte Raspberry Pi3.
För att underlätta installation av Kubernetes och Docker så användes en del förkompilerade versioner av både Kubernetes samt Docker. För Kubernetes användes kube-config (https://github.com/luxas/kubernetes-on-arm) och för Docker användes en förbyggd Docker deb-paket från Hypriot.
Mycket av konfigurationen för Kubernetes får man på köpet via kube-config där man kan installera diverse delar. Flannel installeras till exempel automatiskt, vilket hanterar det overlay-nätverk som används av kubernetes. Så i det här projektet blev det lite av en genväg för att snabbt komma igång och slippa kompilera hela Kubernetes för ARM.
Hypriot tillhandahåller även förbyggda image-filer med Docker på. Men vid tillfället för projektet så fanns det ingen förbyggd image med stöd för WiFi på Raspberry Pi3 korten och där av valdes att köra en ren Raspbian image (Debian byggt för Arm riktat mot Raspberrykorten). (I skrivande stund har det släppts en uppdaterad image från Hypriot-teamet där det ska funka bra att boota från WiFi på Rpi3.)
För att övervaka klustret så användes ett verktyg som heter Netdata. Det är ett open source verktyg som presenterar data på realtidsuppdaterade sidor. För projektet byggdes en egen sida där alla noders data presenterades i realtid. Även temperaturen för alla korten presenteras på sidan. Den så kallade ”kube.html” sidan finns att ladda ner under kapitlet ”Projektfiler”.
Nedan är merparten av de delar som användes för bygget av klustret:
(Röda kabel-sleeves samt kylflänsar beställdes från Kina. Övriga delar köptes från diverse svenska företag.)
Tanken med klusterlådan var att få den så liten som möjlig samtidigt att få till ett utseende som smälter in i hemmet. Ett första bygge skapades där en Nexus 7 surfplatta skulle användas för övervakning:
När bygget var näst intill färdigt så vägrade plattan att fungera och bygget byttes ut mot ett mindre bygge.
Här bredvid en bild på hur det första bygget blev. Bilden visar hur plattan visar status för alla noder via en realtidsuppdaterad netdata sida. (Det blev en bygglogg för projektet som ni kan se här: http://www.sweclockers.com/galleri/13818-raspberry-pi-cluster.)
Det andra bygget började med att göra iordning lådan som klustret skulle husera i.
Undersidan gjordes iordning för att ha strömförsörjning samt sladdar i. Ovansidan borrades hål i för att få upp sladdarna från undersidan. Locket på lådan fick två stora hål där metallnät sattes in för att filtrera damm då fläkten drar luften genom lådan.
Det som skett efter bilderna togs var att sätta på kylflänsar på korten för att sänka temperaturen. Rpi3 är kända för att bli ganska varma. Därför sattes även en fläkt in i lådan som blåser igenom alla korten.
(Bilderna nedan visar bygget. En mer detaljerad bygglogg hittar ni här: http://www.sweclockers.com/galleri/13821-rpi3-cluster-2.)
![]() |
![]() |
![]() |
![]() |
![]() |
På lådan sitter det fyra lysdioder som visar om en nod är igång eller inte. Varje nod har även en knapp som kan resetta ett kort ifall man behöver göra det.
För att kunna resetta ett Raspberry-kort behöver du löda på stift på resetbryggan och sedan koppla en brytarswitch till den. Du behöver även vara aktsam när du löder på stift då det är väldigt lite utrymme för misstag på undersidan av kortet.
Dioderna sattes på +5V och ground på GPIO bryggan och varje diod har ett motstånd på 350 Ohm fastlött. Alla kablar är sleevade med röda sleeves för att få mer ordning och bättre luftflöde i lådan.
RPi3 kort har inbyggt WiFi och det används för klustret. Detta är kanske inte något du normalt väljer för ett kluster som ska ha en hög tillgänglighet, men eftersom det är ett hemmakluster så är routern lika stor SPOF som korten i det här fallet.
Routern konfigurerades med static DHCP lease för de 4 korten, baserat på MAC filtrering. Varje nod döptes med prefix ”kube-”. En kontroller samt 3st worker-noder.
kube-controller: 192.168.1.150
kube-node1: 192.168.1.151
kube-node2: 192.168.1.152
kube-node3: 192.168.1.153
Det första manuella steget är att skriva in en OS image på SD-korten. Det är ett svårt steg att automatisera ifall man inte har kortläsare med 4 portar för mSDHC (eftersom man behöver byta kort mellan skrivningarna).
Det operativsystem som användes var senaste Raspbian Lite (baserat på Debian Jessie) med kernel 4.4 (2016-05-10). OS-imagen skrevs in på varje kort med dd.
sudo dd bs=1m if=<image_namn>.img| pv |sudo dd of=/dev/disk6
Att pipe:a via kommandot ’pv’ ger status på hur mycket som skrivits till kortet så att man får en uppfattning på hur lång tid det tar.
Notera att det är viktigt att of är korrekt disk så att man inte skriver över fel disk!, i det här fallet var /dev/disk6 SD-kortet.
Efter SD-korten var på plats så bootades klustret upp med trådbundet ethernet för att se så att allt fungerade samt att WiFi gick att aktivera.
Eftersom installationsflödet består av ganska många steg samt på 4 kort så användes Ansible för att automatisera hela installationsflödet så långt det gick. Varje del beskrivs nedan följt av det Ansible script som användes.
Det gör det även enkelt att nollställa systemet ifall något gått fel och se till att allt är konsistent på korten.
(Vill man även flasha om korten så blir det enkelt att installera om allt, bara genom att köra playbooken nedan.)
Vissa script som används av systemet behövde modifieras en aning för att kunna köras automatiskt och ersätts därför av den så kallade Playbook som skapades.
Playbooken har följande flöde:
Nedan är hela ansible playbooken som gör det som beskrivs ovan:
#!/usr/bin/env ansible-playbook
—
– hosts: rpi
gather_facts: yes
vars:
wifi:
ssid: ”mywifisid”
password: ”mywifipassword”
packages_to_install: [ git, automake, gcc, autoconf, make, zlib1g-dev ]
update_cache: yes
sudo: yes
tasks:
– name: Add modified raspi-config
template: src=raspi-config dest=/usr/bin/raspi-config
– name: Check if docker pkg is installed
shell: dpkg -l |grep -i docker
ignore_errors: True
register: docker_exists
– name: Copy docker package
copy: src=docker-hypriot_1.10.3-1_armhf.deb dest=/home/pi/docker-hypriot_1.10.3-1_armhf.deb
when: docker_exists.rc != 0
– name: Install docker pkg
command: dpkg -i /home/pi/docker-hypriot_1.10.3-1_armhf.deb
when: docker_exists.rc != 0
– name: Check if fs is expanded
shell: df -h | grep ”/dev/root 7”
ignore_errors: True
register: expanded_res
– name: Expand root fs
command: raspi-config
when: expanded_res.rc != 0
– name: restart machine
shell: sleep 2 && shutdown -r now
async: 1
poll: 0
sudo: true
ignore_errors: true
when: expanded_res.rc != 0
– name: waiting for server to come back
local_action: wait_for host={{ inventory_hostname }} state=started delay=10 timeout=60
sudo: false
when: expanded_res.rc != 0
– name: install ubuntu packages
apt: pkg={{ item }} state=installed update_cache={{ update_cache }}
with_items: packages_to_install
– name: Assign hostname
raw: ”echo {{hostname|quote}} > /etc/hostname”
– name: Update hosts file
template: src=hostsfile dest=/etc/hosts
– name: Check if swap exists and is not zero size
command: /usr/bin/test -s /swap/swapfile
ignore_errors: True
register: swap_res
– name: Create swapdir
file: path=/swap state=directory mode=0755
when: swap_res.rc != 0
– name: Create swapfile
file: path=/swap/swapfile state=touch mode=0600
when: swap_res.rc != 0
– name: Create swap
command: dd if=/dev/zero of=/swap/swapfile bs=1M count=512
when: swap_res.rc != 0
– name: Make swap
command: mkswap /swap/swapfile
when: swap_res.rc != 0
– name: Swap on
command: swapon /swap/swapfile
when: swap_res.rc != 0
– name: Swappiness
lineinfile: dest=/etc/sysctl.conf line=”vm.swappiness=1″
– name: fstab update
lineinfile: dest=/etc/fstab line=”/swap/swapfile none swap sw 0 0″
– name: Check if kube-systemd is downloaded already
command: /usr/bin/test -s /root/kube-systemd.deb
ignore_errors: True
register: kube_res
– name: download kube-config
get_url: url=https://github.com/luxas/kubernetes-on-arm/releases/download/v0.7.0/kube-systemd.deb dest=/root/kube-systemd.deb mode=0755
when: kube_res.rc != 0
– name: Install kube-config
apt: deb=/root/kube-systemd.deb
– name: Copy modified kube-config
copy: src=kube-config dest=/usr/bin/kube-config mode=0755
– name: Install with modified kube-config
shell: /usr/bin/kube-config install
– name: Check if netdata is cloned already
command: /usr/bin/test -d /home/pi/netdata
ignore_errors: True
register: netdata_res
– name: Checkout netdata
git: repo=https://github.com/firehol/netdata.git dest=/home/pi/netdata
when: netdata_res.rc != 0
– name: check if netdata is installed
command: /usr/bin/test -e /usr/sbin/netdata
register: netdata_exists
ignore_errors: True
– name: Build netdata
shell: cd /home/pi/netdata; echo | /home/pi/netdata/netdata-installer.sh
when: netdata_exists.rc != 0
– name: Copy kube.html page
template: src=kube.html dest=/usr/share/netdata/web/kube.html mode=0755
– name: Update rc.local with autostart of netdata.
lineinfile: dest=/etc/rc.local line=”/usr/sbin/netdata” insertbefore=”exit 0″
when: netdata_exists.rc != 0
– name: put wifi config in place
template: src=wpa_supplicant.conf.j2 dest=/etc/wpa_supplicant/wpa_supplicant.conf
– name: Change pi password
user: name=pi update_password=always password=<mysecret>
– name: restart machines
shell: sleep 2 && shutdown -r now
async: 1
poll: 0
sudo: true
ignore_errors: true
when: expanded_res.rc != 0
För att ansible ska veta vad alla hostar har för IP samt login så skapades filen hosts med följande innehåll:
[rpi]
kube-controller ansible_ssh_host=192.168.1.150 ansible_ssh_user=pi ansible_ssh_pass=mypass host_key_checking=false hostname=kube-controller
kube-node1 ansible_ssh_host=192.168.1.151 ansible_ssh_user=pi ansible_ssh_pass=mypass host_key_checking=false hostname=kube-node1
kube-node2 ansible_ssh_host=192.168.1.152 ansible_ssh_user=pi ansible_ssh_pass=mypass host_key_checking=false hostname=kube-node2
kube-node3 ansible_ssh_host=192.168.1.153 ansible_ssh_user=pi ansible_ssh_pass=mypass host_key_checking=false hostname=kube-node3
Ansibles konfigurationsfil fick följande innehåll för att bland annat peka ut hosts filen:
[defaults]
hostfile = ./hosts
host_key_checking = False
deprecation_warnings = False
Ansibles konfigurationsfil fick följande innehåll för att bland annat peka ut hosts filen:
[defaults]
hostfile = ./hosts
host_key_checking = False
deprecation_warnings = False
När allt var på sin plats kördes playbooken med följande kommando:
ansible-playbook playbook.yml
En fördel med att köra Ansible är att det går att köra samma kommando åter igen och status på alla noder kommer då vara samma efter varje körning. Om t.ex. Kubernetes är avinstallerat på en nod så kommer det vara installerat igen efter nästa körning.
Några manuella steg återstod efter ansible-installation och det var att aktivera klustret. På kube-controller (kontrollernoden) körs kommandot:
kube-config enable-master
Det körs för att säga till kubernetes vilken nod som är kontrollern. På de resterande noderna körs stället:
kube-config enable-worker 192.168.1.150
Det IP som angavs efter kommandot är kontrollernodens IP. Om vi inte anger det så hittar den inte vilken nod som är kontrollernoden.
Med kommandot ”kube-config info” kan man sedan se info om klustret. ”kubectl get no” ger status på alla noder.
För att även få en dashboard till Kubernetes så installerades den via kube-config enable-addon dashboard som då deployas på klustret och går att nå via webbläsaren på url http://192.168.1.150:8080/ui/.
Kube-config kan även användas för att installera en färdig lastbalanserare samt DNS server. Kubectl är det cli-gränssnitt som finns mot kubernetes och används för att skapa podar, services, replication controllers samt se status på diverse komponenter.
Netdata installerades även det via Ansible; en installation per nod + kontroller.
Kube.html anropar via Javascript varje nods netdata URL och presenterar returnerad data på sidan. Där av får man ett gränssnitt för alla noder i klustret som går att nå via nätet. Nedan är ett screenshot på hur det ser ut.
Sidan visar i realtid nätverk RX/TX, RAM, swap, CPU, temperatur, disk r/w. Det går även att lägga in grafer för IO-wait vilket kan vara nyttigt att se var flaskhalsen i systemet är. I det här fallet är det mSDHC korten (se kapitel ”Nästa steg” för hur en HDD kan användas istället).
Ett litet ”problem”, om man kan kalla det så, är att processor-arkitekturen på Raspberry Pi är ARM.
De flesta docker images som finns att tillgå på t.ex. Docker Hub är kompilerade för x86 arkitekturen. Det medför att man exempelvis inte kan skapa en docker-fil med ”FROM: ubuntu” eftersom den kommer basera docker-containern på Ubuntu x86 från Docker Hub.
I det här fallet fanns en skapad image på Docker Hub som teamet bakom Hypriot släppt som även var byggt med NodeJS (hypriot/rpi-node) support.
Om man försöker exekvera en container som är byggd på fel arkitektur får man felet som nedan eftersom det inte går att exekvera binärerna:
För att hantera docker med annan användare än root, i det här fallet användaren ”pi” så måste man lägga in pi till gruppen ”docker”.
sudo usermod -aG docker pi
Där efter kan man använda docker-kommandot för att skapa containers, images etc.
Att bygga en docker-image från ett existerande projekt är ganska enkelt. Det enda man behöver skapa är en Dockerfile i katalogen för projektet. I filen specificerar man vilken bas man vill utgå ifrån (i det här fallet hypriot/rpi-node som baseras på raspbian och node för ARM).
Man specificerar vilken port man vill öppna upp, vilka filer/kataloger som skall ingå samt hur applikationen skall exekveras.
Filen Dockerfile för spelet blev följande:
FROM hypriot/rpi-node
EXPOSE 8081
ADD *.* /qake/
ADD server/ /qake/server/
ADD bootstrap /qake/bootstrap/
ADD images/ /qake/images/
ADD js/ /qake/js/
ADD libs/ /qake/libs/
ADD maps/ /qake/maps/
ADD models/ /qake/models/
ADD sound/ /qake/sound/
WORKDIR ”/qake/server/”
CMD node server.js
I Dockerfile specificeras även WORKDIR vilket medför att CMD exekveras i den sökvägen (utan WORKDIR hade det blivit ”node /qake/server/server.js), vilket kan vara bra beroende på hur applikationen är uppbyggd. Porten 8081 är vad spelet lyssnar på i nodejs servern.
När filen är skapad är det dags att bygga docker-imagen med följande kommando:
docker build –t lallassu/qake.
Det kan ta ett tag första gången då den behöver hämta ner de images som behövs för att bygga.
För att vara säker på att en docker-image fungerar innan man deployar den så är det enkelt att starta igång en container med ett skal:
docker run -it qake /bin/bash
Då får man upp ett bash-skal där man kan testa att exekvera så som ”CMD” anger i Dockerfile. I det här fallet: node server.js
För att enkelt kunna komma åt sina docker-images så kan man använda sig av de tjänster som finns. Gcr.io är en tjänst från Google (Google Cloud Registry).
En annan är Docker Hub (docker.io). På docker.io får man ett privat-repo vilket används i det här fallet. Man kan också skapa ett eget registry lokalt ifall man föredrar det.
För att komma åt ett privat docker.io repository behöver man skapa en så kallad secret i kubernetes. Det gör man på följande vis:
kind: Secret
metadata:
name: registrypullsecret
data:
.dockerconfigjson: <base64_string>
type: kubernetes.io/dockerconfigjson
Den skapade ”secreten” kommer användas i konfigurationen för pod:ar för att få access till docker-registry då en image ska hämtas.
Det är enkelt att ladda upp sin image till docker.io via kommandot:
docker push lallassu/qake
Kubernetes pod
En pod i kubernetes är en eller flera containers som delar storage och körs på samma underliggande hårdvara. För att skapa en pod med en container av spelet så skapades följande konfigurationsfil. Här används YAML format men även JSON går att använda (qake.yml):
apiVersion: v1
kind: Pod
metadata:
name: qake
spec:
containers:
– name: private
image: lallassu/qake:latest
imagePullSecrets:
– name: registrypullsecret
Det man specificerar i den här filen är namnet på pod:en. Vilka containers som ska vara med samt vilken image som ska användas. Även vilken secret som ska användas anges så att det går att hämta den image man vill ha från docker-registry (docker.io).
Podden kan sedan skapas med kommandot:
kubectl create –f qake.yml
Det kommandot går fort men pod:en är inte nödvändigtvis uppe för det. För att se status på poden kan man använda kommandot:
kubectl get pods
Vill man ha mer information om vilka events som sker kan man kolla med describe på följande vis:
kubectl describe qake
För att enklare skapa skalning av pod:ar så kan man skapa en replication-controller. Det sker på liknande vis som för en pod. Följande fil skapades (även den i YAML format):
apiVersion: v1
kind: ReplicationController
metadata:
name: qakerc
spec:
replicas: 2
selector:
app: qake
template:
metadata:
name: qake
labels:
app: qake
spec:
containers:
– name: qake
image: docker.io/lallassu/qake:latest
ports:
– containerPort: 8081
imagePullSecrets:
– name: registrypullsecret
En av de stora ändringarna här mot i pod-filen är att vi specificerar ”replicas”. Det vill säga att vi vill köra två uppsättningar av poden (det går självklart att specificera fler). Det medför att vi får två IP:n att gå mot för båda replikor:
pi@kube-controller:~ $ kubectl describe svc
Name: qakerc
Namespace: default
Labels: app=qake
Selector: app=qake
Type: ClusterIP
IP: 10.0.0.102
Port: <unset> 8081/TCP
Endpoints: 10.1.64.2:8081,10.1.71.4:8081
Session Affinity: None
No events.
I det här fallet tilldelades det följande IPn med portar: 10.1.64.2:8081,10.1.71.4:8081. Varje pod får ett helt nät tilldelat sig och därav blir det olika nät som pod:arna ligger på.
Kubernetes kommer inte med någon lastbalanserare utan det förväntas man tillhandahålla själv. Det finns flera alternativ, t.ex. Calico som har stöd för GBP, HA-Proxy etc. Molnleverantörer brukar leverera lastbalanserare som man kan använda sig av.
I det här fallet skapades endast enkla iptables-regler för att komma åt noderna externt för att se att det fungerade som det skulle:
iptables -A PREROUTING -t nat -i wlan0 -p tcp –dport 8085 -j DNAT –to 10.1.64.2:8081
iptables -A FORWARD -p tcp -d 10.1.64.2 –dport 8085 -j ACCEPT
Efter mycket jobb med att hitta rätt byggstenar som är kompilerade för ARM så gick till slut spelservrarna igång. Nedan är två screenshots som visar dels klienten (webläsaren)
där spelet körs samt servern som hanterar spelarna.
Kubernetes kommer även med en dashboard som det går att deploya applikationer via. Den är inte lika kraftfull som kubectl men ger en bra överblick på vad som körs etc. Nedan är några screenshots från panelen:
Kubernetes plattformen är en stor och avancerad produkt som har stöd för autoskalning. Nästa steg är att autoskala spelservrarna. Helst behöver man även autoskala underliggande hårdvara vilket skulle gå bra via API på Cygate Hosted VMware eller Elastic Cloud Server.
Att använda mSDHC kort, som i det här fallet inte var så snabba, är lätt att byta ut till en hårddisk. Man kan då ”jump-boota” från SD-kortet till disken genom att ändra i /boot/cmdline.txt från följande:
dwc_otg.lpm_enable=0 console=ttyAMA0,115200 console=tty1 root=/dev/sda1 rootfstype=ext4 elevator=deadline rootwait
Till att istället ange disken som root:
dwc_otg.lpm_enable=0 console=ttyAMA0,115200 console=tty1 root=/dev/mmcblk0p2 rootfstype=ext4 elevator=deadline rootwait
Förutom risken för hög IO-wait mot disk då man kör mSDHC så riskerar man att de skrivs sönder över tid. Av egen erfarenhet skrivs ett kort sönder efter c:a 1år även om man kör utan swap samt lägger flyktiga kataloger i ramFS (/tmp/, /var/ etc).
Att lägga in en lastbalanserare framför klustret är även ett steg att göra, så att man enkelt kan lastbalansera mellan sina podar.
Det finns fortfarande en uppsjö av kunskap att fördjupa sig i när det kommer till Kubernetes, så som autoskalning av sina pod:ar. Det här projektet har bara skrapat lite på ytan.
Detta var ett roligt projekt som ger mersmak på att fortsätta undersöka diverse plattformar för att managera docker-containers.
Nästa del skulle kunna bli att se över Docker Swarm som är Docker-projektets egna typ av manageringsverktyg för containers?
Raspberry Pi är trevliga små enkortsdatorer med bra prestanda för att vara så billiga. De kan användas till mycket, kolla gärna in mina andra projekt där jag använt Raspberry Pi:
Som en extra bonus kammade projektet hem en tredje plats i Corsairs ”Månadens galleri
i juli 2016” på Sweclockers!
Läs mer på Sweclockers hemsida:
http://www.sweclockers.com/forum/post/16195920
Alla filer för projektet går att ladda ner här:
http://qake.se/project_files.tar.gz
Spelets hemsida: http://qake.se
Netdata: https://github.com/firehol/netdata
Kubernetes: http://kubernetes.io/
Hypriot: http://blog.hypriot.com/
Docker: https://www.docker.com/
Docker.io: https://hub.docker.com/
Bygglogg 2:a bygget: http://www.sweclockers.com/galleri/13821-rpi3-cluster-2
Bygglogg 1:a bygget: http://www.sweclockers.com/galleri/13818-raspberry-pi-cluster
Raspbian Lite: https://downloads.raspberrypi.org/raspbian_lite_latest
Kubernetes on ARM: https://github.com/luxas/kubernetes-on-arm
Kube-systemd: https://github.com/luxas/kubernetes-on-arm/releases/download/v0.7.0/kube-systemd.deb
/Magnus Persson, Cygate Cloud