här

HA Kluster med CoreOS & Docker
Magnus Persson 2014-08-01
Intro
Många har troligen varit med om att deras tjänster gått ner på grund av olika anledningar. Det kan vara
allt från tillfälliga problem hos leverantören av tjänsten till buggar i applikationen. Med hjälp av ett
CoreOS kluster kan man komma runt de här problemen genom att sprida ut sin applikation på
klustrerade servrar.
CoreOS är en Linux distribution som är byggt för att skala upp och ner på ett snyggt sätt med minimal
overhead vad gäller systemresurser. CoreOS är ett open source project. Eftersom CoreOS fortfarande är
i beta-stadie så finns en hel del workarounds och saker som förväntas bli bättre i kommande versioner.
Det som skiljer sig från en vanlig Linux distribution är att det saknas pakethanterare och alla
applikationer är tänka att köra via docker containers. Docker bygger på LXC (Linux Containers) som är
slags virtualisering på operativsystemsnivå som gör det möjligt att köra flera isolerade instanser av
Linux på en och samma host (behöver ej vara samma Linux distribution som underliggande OS).
CoreOS använder sig av en distribuerad version av system init, som heter systemd. Det använder sig
även av etcd som är en distribuerad key-value-store där man kan spara applikationsspecifik information
som alla noder i klustret kan skriva/läsa.
I den här artikeln skapas ett enkelt testkluster på Ipeer Hosted VMware. För enkelhetensskull så skapas
i det här fallet alla noder på samma plattform, vilket inte är att rekomendera i en sådan här uppsättning
där man bör sprida sina noder på olika plattformar (t.ex. dedikerad server eller en annan molntjänst).
En docker container kommer att skapas med en NodeJS applikation som presenterar ett litet webbaserat
spel. Applikationen har även en inbyggd webserver.
CoreOS Installation
Steg 1 är att installera CoreOS. Det här går att göra på flera sätt, här har vi valt via ISO fil som laddas
upp på vCloud och monteras till en ny VM. Välj statiskt IP manuellt och välj ett ledigt IP till servern.
Dock så får maskinen vid start ett IP via DHCP även om man valt ett specifikt IP. Detta gör inget då vi
använder vCloud consolen för att komma åt vår VM. Då den bootar upp så får man ett skal där man
kan installera CoreOS till disk genom att köra:
coreos-install -d /dev/sda
När installationen är klar så är det bara tills vidare att stoppa servern och avmontera ISO filen.
Nu kommer ett liten gotcha. CoreOS installeras utan möjlighet att logga in. CoreOS läser sin
konfiguration från en så kallad cloud-config fil. Det är en YAML fil som definierar konfiguration som
noden ska använda sig av. I den här filen kan man även konfigurera användare samt lägga in SSH
nycklar (även från git-hub!). För enkelhetens skull har vi valt att använda lösenord, men
rekommenderar att använda SSH nycklar.
Cloud-config filen ser ut så här:
#cloud-config
hostname: node
coreos:
etcd:
name: node
discovery: https://discovery.etcd.io/c7c57c96841f7f13887fcad4bd904b6d
addr: 172.16.0.10:4001
peer-addr: 172.16.0.10:7001
update:
reboot-strategy: etcd-lock
fleet:
metadata: provider=ipeer
units:
- name: etcd.service
command: start
- name: fleet.service
command: start
- name: 10-network.network
content: |
[Match]
Name=ens*
[Network]
Address=172.16.0.10/24
Gateway=172.16.0.1
DNS=8.8.8.8
users:
- name: core
passwd:
$6$SALT$pt6375Q0FdjCkYhFrsbS9oG0cmFcWtD/.t5v6CBUm.XFsjZn1AgxQHELLPLNBmdAluDsGrze06HdFR4rMAJ
AX1
groups:
- sudo
- docker
Notera att det är väldigt viktigt med att ha indenteringen korrekt I YAML filer, en tab eller fel antal
mellanslag kan göra filen korrupt.
I filen ovan specifieras host-namnet på noden via 'hostname' (I det här fallet används 'node' följt av
inkrementellt nummer). IP adressen kan sättas via variabeln $public_ipv4 eller $private_ipv4 men det
fungerar inte så bra i nuvarande version ifall man vill köra DHCP. Därför har vi även valt att använda
statiskt IP.
En viktig del är att använda sig av en discovery vilket det finns en publik tjänst för där man kan skapa
en ny nyckel genom att gå till: https://discovery.etcd.io/new. Då generas en unik nyckel som man
sedan kan använda som discovery nyckel med adressen: https://discovery.etcd.io/<nyckel>
Går man sedan till den adressen kan man se vilka noder som är med I klustret. Nedan visas en bild där
ingen nod är med I klustret, ännu.
En till sak som är lite unikt med CoreOS är att det uppdaterar sig själv och startar då automatiskt om.
Därför konfiguras en uppdateringsstrategi, I det är fallet 'etcd-lock' vilket innebär att klustret ser till att
inte starta om sig ifall någon annan nod är nere eller för tillfället uppdaterar sig, så att klustret hela
tiden har minst en aktiv nod. Efter detta specifieras ett antal systemd units som ska starta, i det här
fallet fleet (som kommer hanteras lite senare I den här artikeln) samt etcd. Vi specifierar även
nätverksinställningar så som IP och en DNS server, I det här fallet Googles 8.8.8.8 DNS server.
Sist konfiguerar vi upp en användare med lösenord som är en hash av lösenordet. I skrivande stund så
har fleet bara stöd för hårdkodade användaren ”core”, även om det går att skapa andra användare. Hash
av lösenordet går att generera t.ex. med perl:
perl -e 'print crypt("<ditt lösenord>","\$6\$SALT\$") . "\n"'
Nu när cloud-config är skapad så ska den läggas in på noderna. Detta medför lite pyssel eftersom vi
inte kan logga in och lägga in manuellt (vid manuellt så ska cluster-config placeras i
/usr/share/oem/cluster-config.yml på noderna). Men vi kan skapa en ISO fil med konfigurationen som
görs enligt instruktionerna för ”Config Drive” (https://github.com/coreos/coreoscloudinit/blob/master/Documentation/config-drive.md).
Det görs på följande sätt:
nergal@calypso:~/Work/CoreOS/cluster$ ls new-drive/openstack/latest/user_data
new-drive/openstack/latest/user_data
nergal@calypso:~/Work/CoreOS/cluster$ mkisofs -R -V config-2 -o configdrive.iso new-drive/
I: -input-charset not specified, using utf-8 (detected in locale settings)
Total translation table size: 0
Total rockridge attributes bytes: 677
Total directory bytes: 4458
Path table size(bytes): 40
Max brk space used 22000
178 extents written (0 MB)
nergal@calypso:~/Work/CoreOS/cluster$ ls configdrive.iso
configdrive.iso
nergal@calypso:~/Work/CoreOS/cluster$
'user_data' innehåller cloud-config datat. 'configdrive.iso' kan sedan laddas upp till vCloud som en
vanlig ISO fil. Den monteras sedan till CoreOS VM:en och efter en omstart så laddas konfigurationen
in automatiskt och vi kan nu logga in med den skapade användaren. Här ser vi även att vårt statiska IP
är satt (172.16.0.10).
Då har vi vår första CoreOS server startad. Upprepa samma procedur för andra noden och konfigurera
om cloud-config (user_data) filen att innehålla nytt hostname/nodename samt annat IP. Det går även att
kopiera node1 via vCloud 'copy-to' till en ny server och konfigurera om denna samt ladda in cloudconfig filen för node2. Vi skapar även en tredje server, kallad node3, för att enkelt kunna visa hur
failover fungerar senare.
När vi väl startat alla våra servrar kan man gå till discovery URL:en och se så att alla servrar är startade
I klustret. Andra bilden visas våra servrar i vCloud kontrollpanelen.
Docker Containers
Nu ska vi fixa oss en docker image för vårt spel som vi ska hosta på klustret. Notera att en dockerinstans kan innehålla nginx, apache eller vad man nu vill köra. I vårt fall har vi en nodejs server som
skall köras.
Först skapar vi en docker konfigurationsfil för vår nya image som ska döpas till Dockerfile. Docker
containern kommer köra Ubuntu 14.04 och behöver nodejs samt sqlite3. Vi vill även att containern ska
publicera porten 443 (som nodejs servern lyssnar på). Vi säger sedan till docker att lägga in allt i
katalogen vi står i, vilket är hela applikationen. Vi specifierar även vad som skall exekveras då
containern startas, I det här fallet ”server.js”.
Filen ser ut så här:
# DOCKER-VERSION 0.9.1
FROM ubuntu:14.04
RUN apt-get update
RUN apt-get install -y npm nodejs sqlite3
ADD . /
EXPOSE 443
CMD ["nodejs", "/server/server.js"]
Ubuntu 14.04 hämtas från Dockers bas-registry som hanteras av docker teamet och anses som 'trusted'.
I det är fallet kommer vi använda vårt egna publika registry konto för att hantera den image vi ska
använda. Notera att man kan enkelt skapa ett eget privat repository på Dockers hemsida. Vi bygger en
ny image och pushar upp på vårt konto (efter att ha loggat in med 'docker login'):
sudo docker.io build -t lallassu/saving-nemo .
sudo docker.io push lallassu/saving-nemo
nergal@calypso:~$ sudo docker.io images
REPOSITORY
TAG
IMAGE ID
lallassu/saving-nemo
latest
1ca2d5337ea4
CREATED
3 minutes ago
VIRTUAL SIZE
420.5 MB
Där har vi nu vår applikation som en docker container som skall köras på CoreOS klustret. För att se så
att den går att nå från vårt kluster kan vi logga in på CoreOS servern och hämta hem den:
docker pull lallassu/saving-nemo
Sedan kan vi även testa att containern funkar att starta på noden (se bild):
docker run –rm -t lallassu/saving-nemo
NodeJS servern startas som förväntat.
Men vi vill inte starta docker containern manuellt och den ska även ha HA samt starta om ifall den
kraschar. Det är här fleet kommer in i bilden. Fleet är ett abstraktionslager till systemd och ser till att en
service körs på vissa noder baserat på några enkla regler man specifierar. Det kan vara regler som att
man inte vill att en viss typ av docker container körs på samma nod som en av samma typ utan ska vara
på åtskilda noder. Vi kan även specifiera hur en container ska startas om, om något ska göras innan
uppstart etc. Man kan även spcecifiera med meta-data vart vissa typer ska köra och vilka krav de har.
Fleet konfigureras med unit filer som har samma syntax som systemd unit filer. I vårt fall har vi skapat
en fil som först specifierar att innan uppstart ska senaste imagen hämtas ifall det finns någon nyare från
docker registry. Sedan konfigureras hur containern ska stoppas och startas.
Vår unit fil (döpt till nemo.1.service) ser ut som följer:
[Unit]
Description=nemo-server
After=docker.service
Requires=docker.service
[Service]
ExecStartPre=/usr/bin/docker pull lallassu/saving-nemo
ExecStart=/usr/bin/docker run --name nemoserver --rm -p 8085:443 lallassu/saving-nemo
ExecStop=/usr/bin/docker stop -t 1 nemoserver
Restart=always
[X-Fleet]
X-Conflicts=nemo.*.service
X-ConditionMachineMetadata=provider=ipeer
Här har vi specifierat att vår service måste starta efter att docker servicen har startat då det är ett krav
för att vi ska kunna starta containern. Vi specifierar att den externa porten för containern ska vara 8085
samt sätter ett namn på containern (nemoserver). Vi säger även att den ska startas om vid failure, hur
den än falerar. Den här konfigurationen är egentligen systemd konfiguration. Det som är fleet specifikt
kommer under X-Fleet delen. Här säger vi att servicen inte får köra på samma nod som någon annan av
samma service samt att noden som den körs på måste ha provider ipeer (meta-data specifierade vi i
cloud-config filen för CoreOS tidigare). Via meta-data kan vi specifiera vilken typ av hårdvara noden
har eller vart i världen noden kör och sedan basera våra val av instanser på den specifierade meta-datan.
Exempelvis ifall vi bara vill köra på noder i Sverige med dubbla cores skulle vi kunna specifiera metadatan ”country=sweden,vcpu=2” och sedan sätta detta som ett krav (X-ConditionMachineMetaData) i
vår unit-fil.
För att aktivera en unit fil med fleet används 'fleetctl' på en node. Samma output kommer visas på alla
noder i klustret. Här är några användbara kommandon för fleet.
fleetctl start <unit file> # starta unit
fleetctl stop <unit file> # stoppa unit
fleetctl list-machines -l # lista alla noder I klustret
fleetctl list-units # lista alla units
fleetctl journal <unit file> # loggar för en unit
fleetctl ssh <machine_id> #logga in via ssh på en nod (kräver ssh-agent samt endast med ssh-nycklar)
Vi kan nu testa att starta vår unit, se bild:
Som vi ser på bilden startas den här uniten på 172.16.0.10 noden. För att skapa en likadan unit så är det
bara att kopiera filen nemo.1.service till nemo.2.service utan att göra någon ändring i den. Vi kan sedan
starta upp även denna service, se bild nedan:
Man kan då se att vår andra service-unit startas på en annan nod vilket stämmer med vår konfiguration
där ingen service av typen nemo.x.service fick köras på samma nod (X-Conflicts=nemo.*.service).
Vi kan nu testa att stoppa vår container med docker (docker stop <id>), vilket triggar fleet att starta om
servicen. Som vi ser på bilden nedan så startas docker containern (unit) om direkt.
Eftersom vi har 3 noder i klustret så ska vi se om en failover fungerar. Vi startar om node3
(172.16.0.12). Den service-unit som körs (nemo.2.service) där kommer starta upp på node2
(172.16.0.11). Eftersom containern inte har körts tidigare på node2 så måste docker imagen hämtas från
docker registry vilket tar lite tid, där av ligger servicen i 'start-pre' status ett tag. Detta kommer inte ske
i omstarter i framtiden (om ingen image uppdatering är gjord). Efter start-pre går servicen upp i
running.
Lastbalanserare
Nu när vårt kluster är uppe behöver vi ha en lastbalanserare framför som använder sig av vårt kluster.
Det finns flera alternativ men I vårt fall använder vi vClouds ingbyggda och skapar en ny pool med
våra klusternoder. Under Edge Gateway i vCloud kontrollpanelen kan man välja ”configure-services”
och där under ligger en flik med namnet ”Load Balancer”. Här skapar vi en ny lastbalanserare som
använder Round Robin balansering mot port 8085 på http och TCP (vår applikation avänder även
socket.io via TCP).
I nästa steg lägger vi till våra CoreOS noder med IP som vi konfigurerat för varje nod.
När vi lagt till alla noder har vi följande uppsättning I lastbalanseraren.
Sedan behöver vi ha en front som lyssnar på ett extern port och tar hand om alla requests, vilket vi
lägger in under ”Virtual Servers” under ”Load Balancer” fliken. Här konfigurerar vi vårt externa
organisations IP att lyssna på porten 8090 och att använda poolen ”CoreOS LB” som vi skapade
tidigare.
För att testa att vår lastbalanserare fungerar går vi mot vårt externa IP, I vårt fall 81.91.13.34 och mot
porten vi valde (8090), http://81.91.13.34:8090. För en riktig lösning med webserver har man troligen
port 80 och/eller 443 istället för port 8090.
I vår applikation kan vi se inkommande TCP uppkopplingar som sker med socket.io från vår webläsare
och kan där följa om det kommer in några till våra docker containers. Vi attachar oss till de containrar
som körs för att se output till stdout från vår applikation.
Som vi ser på bilden lastbalanseras inkommande connections till båda våra noder och Docker
containrar vid omladdning av sidan. Allt fungerar som tänkt!
Uppdatera Docker containern
Tidigare definierade vi ExecStartPre I unit filen för våra fleet services. Där specifierade vi att docker
skulle hämta hem senaste versionen av vår image. Det gör det möjligt att uppdatera vår image på ett
enkelt sätt och sedan commita ändringar som vi pushar upp till vårt registry. När vi sedan startar om
våra service:ar via fleet så hämtas den nya versionen av vår Docker image och startas på klustret.
Summering
Vi har nu satt upp ett HA kluster med självuppdaterande operativsystem vid namn CoreOS, använt
isolerade LXC containrar via Docker och managerat vårt kluster med Fleet samt konfigurerat en
lastbalanserare som nyttjar vårt kluster. Vårt spel har nu hög HA och skulle en instans krasha startas det
om automatiskt och skulle en nod gå ner finns det andra som tar över. Skulle vi sedan sprida våra noder
på andra ställen I världen eller på andra molntjänster/servrar skulle vi få än högre HA.
En enkel skiss på hur klustret är uppbyggt:
Att tänka på
- DHCP stöds inte på ett bra sätt ännu av CoreOS
- Fleet har hårdkodat användare för SSH vilken är ”core”. Vilket ställer till lite problem vid 'fleetctl ssh
<machine_id>' om man har en annan användare.
- SSH nycklar behöver kopieras och användas för att fleetctl ssh ska fungera.
- eval `ssh-agent`; ssh-add behöver exekveras för att fleet ska kunna ssh:a med nycklar via en sshagent till noderna.
- Vissa portar kan behöva öppnas upp i firewallen i vCloud.
Länkar
http://stable.release.core-os.net/amd64-usr/current/coreos_production_iso_image.iso
http://www.coreos.com
https://github.com/coreos/fleet
https://github.com/coreos/etcd
https://www.docker.com/
https://linuxcontainers.org/
https://github.com/coreos/coreos-cloudinit/blob/master/Documentation/config-drive.md