CoreOS ist eine Linux-Distribution, die speziell darauf ausgerichtet ist, Docker-Container in Produktion zu betreiben. Dieser Artikel gibt einen Überblick über die CoreOS-Tools. Darüber hinaus wird diskutiert, welche Konzepte man verwenden kann, um auch größere Anwendungen auf einem CoreOS-Cluster zu betreiben.
Das Ökosystem rund um Docker ist im letzten Jahr förmlich explodiert. Viele neue Tools und Plattformen erleichtern nun den Umgang mit Containern für verschiedene Anwendungszwecke. Mit CoreOS entwickeln Alex Polvi und Brandon Philips eine spannende neue Linux-Distribution, welche darauf ausgerichtet ist, eine containerbasierte Cloud-Infrastruktur aufzubauen. Hierbei war eine erste Idee der beiden, ein Betriebssystem für Server zu entwickeln, das sich (ähnlich wie Chrome/ChromeOS) selbstständig aktualisiert. Davon versprachen sie sich im Ergebnis eine sicherere Infrastruktur für Webapplikationen, da das Basissystem ständig aktuell gehalten und die Software selbst in Containern isoliert wird. In diesem Artikel befasse ich mich jedoch vornehmlich mit dem Betrieb und der Verteilung von Containern auf CoreOS-Clustern.
Beschäftigt man sich mit Docker, kommt sehr schnell die Frage auf, wie Docker auf mehreren Hosts funktioniert. Typische Anforderungen an Services wie Verfügbarkeit und Skalierbarkeit erfordern die Möglichkeit, Container über mehrere Hosts zu verteilen. Der Docker Daemon startet derzeit die Container nur auf seinem eigenen Host und auch Docker-Links, um verschiedene Docker-Container zu verbinden, funktionieren nur auf einem Host. CoreOS bringt dazu zwei wichtige Eigenentwicklungen mit: Etcd [1] und Fleet [2].
Etcd ist ein Key-Value Store, mit dem Fokus auf konsistenter Datenhaltung. Etcd läuft auf jedem Host im Cluster und „verbindet“ die Hosts untereinander. Dort befinden sich die Informationen über den Zustand des Clusters (also beispielsweise welche Hosts existieren, welche Services auf welchem Host laufen etc.). Damit sich die einzelnen Etcd Peers finden, gibt es einen Discovery-Service, der ein HTTP-API für die Peers bereitstellt, an der diese sich anmelden und über die anderen Peers informiert werden. Das Discovery-API kann selbst gehostet werden, oder man benutzt die offizielle Website von CoreOS [3].
Fleet benötigt Etcd und kümmert sich darum, Services auf den Hosts zu verteilen und zu überwachen. Dazu benutzt es Systemd, ein Init-System, welches sich mittlerweile in fast allen großen aktuellen Linux-Distributionen befindet. Die Services beschreibt man in Service-Units, die z. B. das zu startende Kommando und die Abhängigkeiten definieren. Fleet selbst erweitert die Systemd-Servicebeschreibungen um eine Sektion, in der man Einfluss auf die Verteilung der Services auf den Hosts im Cluster nehmen kann. Es kann beispielsweise bestimmt werden, dass Services immer/niemals gemeinsam auf einem Host laufen oder dass der Service z. B. nur auf einem Host mit bestimmten Metadaten laufen darf.
Beide Tools werden in der Sprache Go entwickelt und die Projekte können öffentlich auf GitHub eingesehen werden. An der Entwicklung kann man sich aktiv beteiligen. Ähnlich wie Docker stehen die Projekte unter der Apache 2.0 License.
CoreOS läuft bereits auf sehr vielen Plattformen. Offiziell werden die Clouds von Amazon, Google, DigitalOcean, Azure und Rackspace, aber auch PXE, OpenStack, Apache CloudStack und Vagrant unterstützt. Community-Images gibt es bereits für Brightbox, VULTR, VMware, libvirt und weitere. Um für den Start eines CoreOS-Clusters eine einheitliche, von der unterliegenden Virtualisierung unabhängige Konfiguration zu haben, gibt es eine Cloud-Config-YAML (Listing 1). In dieser YAML kann man die CoreOS-eigenen Komponenten konfigurieren, Nutzer hinzufügen sowie eigene Services definieren. Diese Cloud-Config übergibt man üblicherweise an eine CoreOS-Instanz. Das ist aber abhängig von der verwendeten Virtualisierungstechnologie oder vom Cloud-Anbieter. Als Beispiel wird bei der AWS-Cloud-Formation von CoreOS [4] eine Cloud-Config in die Launch Configuration des Autoscalers gehangen, sodass jede Instanz des Autoscalers die Cloud-Config als Userdata erhält und damit automatisch in den Cluster gehangen wird. Wichtig ist hier, dass z. B. für jeden neuen Cluster ein neues Discovery-Token verwendet wird.
Listing 1: „cloud-config.yaml“
#cloud-config
coreos:
etcd:
# generate a new token for each unique cluster from https://discovery.etcd.io/new
discovery: https://discovery.etcd.io/<token>
# multi-region deployments, multi-cloud deployments, and droplets without
# private networking need to use $public_ipv4
addr: $private_ipv4:4001
peer-addr: $private_ipv4:7001
units:
- name: etcd.service
command: start
- name: fleet.service
command: start
Möchte man einmal mit einem CoreOS-Cluster arbeiten, bieten sich auf dem lokalen Rechner die CoreOS Vagrant Images [5] an. Aber nicht jeder hat Vagrant installiert, und deshalb teste ich persönlich die neuen CoreOS-Instanzen von DigitalOcean [6]. Hat man bereits Erfahrung mit AWS, kann man auch die offizielle Cloud-Formation von CoreOS verwenden. Das gilt natürlich auch für die anderen Cloud-Anbieter. Aber auch auf alternativen Anbietern sollte das Starten von CoreOS keine große Schwierigkeit darstellen.
Ist der Cluster gestartet, kann man mit dem Fleet-Client (fleetctl) darauf zugreifen. Am einfachsten verbindet man sich per SSH zu einem der CoreOS-Hosts, da dort bereits fleetctl installiert ist. Soll der Cluster von einem anderen Rechner aus gesteuert werden, muss ein Fleet-Release [7] heruntergeladen werden. Darin befindet sich das entsprechende fleetctl, und der Cluster kann über einen SSH-Tunnel administriert werden: $ fleetctl --tunnel my-digital-ocean-ip list-machines.
Als Erstes will man einen einfachen Service starten. Der einfachste Service besteht aus einem Docker-Container wie in Listing 2, der auf einem beliebigen Host gestartet werden kann. Der Container ist sehr klein und sollte, wenn er erfolgreich läuft, ein 'hello' zurückgeben. Der dazugehörige Code findet sich auf GitHub [8]
Listing 2: „ping.service“
[Unit]
Description=Ping
[Service]
ExecStart=/usr/bin/docker run --rm -p 80 --name %n giantswarm/ping
ExecStop=/usr/bin/docker stop %n
Restart=always
$ fleetctl start ping.service
Mit fleetctl list-units sieht man nun, dass auf einem Host im Cluster der Service gestartet wurde. Da das Image noch nicht auf dem Host existiert, wird es zuerst heruntergeladen. Wenn der Download abgeschlossen ist, startet der Container mit dem Service. Einen Status über diese Aktion kann Fleet derzeit noch nicht darstellen, da die dafür benötigten Informationen noch nicht von Docker an Systemd weitergegeben werden:
$ fleetctl journal -f ping.service
-- Logs begin at Mon 2014-09-15 19:44:36 UTC. --
Sep 16 12:59:23 systemd[1]: Starting Ping...
Sep 16 12:59:23 systemd[1]: Started Ping.
Sep 16 12:59:23 docker[809]: Unable to find image 'giantswarm/ping:0.0.1' locally
Sep 16 12:59:23 docker[809]: Pulling repository giantswarm/ping
Sep 16 12:59:36 docker[809]: 2014/09/16 12:59:36 web.go serving 0.0.0.0:80
Im Journal des Service sieht man den Download des Images und dann den Start des Service selbst. Der Service ist abzufragen, indem man sich via fleetctl ssh mit dem Host verbindet, auf dem der Service gestartet wurde und dann die IP-Adresse des Servicecontainers mit Curl anfragt:
$ fleetctl ssh ping.service 'curl $(docker inspect --format "{{ .NetworkSettings.IPAddress }}" ping.service)'
Analog kann mit fleetctl stop der Service wieder gestoppt werden. Um die Systemd-Servicebeschreibung noch etwas zuverlässiger zu gestalten, fügen wir noch ein paar Zeilen hinzu. Zuerst definieren wir eine Environmentvariable mit dem Namen des Images, da es mehrfach benutzt wird. Zudem stellen wir sicher, dass kein anderer Container mit dem gleichen Namen läuft oder existiert. Wenn das Image noch nicht auf dem Host vorhanden ist, wird es automatisch bei einem docker run heruntergeladen. Systemd sagt aber, dass der Service bereits läuft und macht mit den Abhängigkeiten weiter. Deshalb laden wir das Image vorher explizit herunter und starten dann erst den eigentlichen Container. Diese Best Practices findet man auch in den Beispielen in der CoreOS-Dokumentation [9].
Listing 3: „ping-pro.service“
[Unit]
Description=Ping
[Service]
Environment="IMAGE=giantswarm/ping:latest"
TimeoutStartSec=0
ExecStartPre=-/usr/bin/docker kill %n
ExecStartPre=-/usr/bin/docker rm %n
ExecStartPre=/usr/bin/docker pull $IMAGE
ExecStart=/usr/bin/docker run -p 80 --name %n$IMAGE
ExecStop=/usr/bin/docker stop %n
Restart=always
In den Servicebeschreibungen empfiehlt es sich, Restart=always, TimeoutStartSec=0 zu benutzen. Damit kann sichergestellt werden, dass es ein unendliches Timeout gibt, damit auch etwas größere Docker-Images heruntergeladen werden können und der Service immer wieder neu gestartet wird, falls der Container stirbt. Das unendliche Timeout geht aber leider auch mit dem Risiko einher, dass der Service nicht von alleine neu gestartet wird, wenn eine der ExecStartPre-Anweisungen „hängen“ bleibt.
Ein einzelner Service ist natürlich nicht sonderlich spannend. Interessanter ist es, mehrere voneinander abhängige Services zu starten. Hierzu gibt es eine Vielzahl von Möglichkeiten. Zur Erläuterung nehmen wir diesmal eine einfache 3-Tier-Webapplikation, die aus Load Balancer, einer Webapplikation und Datenbank besteht. Die Webapplikation benötigt Zugriff auf die Datenbank, und der Load Balancer muss wissen, wo die Webapplikation ist.
Einen kleinen Fallstrick gibt es bei der Cloud-Formation auf AWS. In der CoreOS SecurityGroup werden nur die Ports für Etcd (4001, 7001) zwischen den Maschinen erlaubt. Um auch die (automatisch vergebenen) Ports von Docker zu erlauben, muss man eine weitere Inbound-Regel vergeben, welche die Ports 49000–50000 für die Instanzen freigibt. Dies kann nachträglich im Webinterface getan werden. Hierfür öffnet man in der AWS-Console die EC2-Übersicht und sucht in den SecurityGroups die richtige heraus. Standardmäßig heißt die Gruppe <Cloudformation Name>-CoreOSSecurityGroup-*. Nach der Anwahl der Gruppe fügt man über Actions->"Edit inbound rules" eine weitere Regel hinzu. Die Quelle ist dabei die ID der SecurityGroup selbst. Dadurch werden die Ports nur für Mitglieder dieser Gruppe selbst freigegeben und nur die Instanzen selbst können darauf zugreifen.
Unsere Aufgabe ist es nun, die verschiedenen Services miteinander zu verbinden. Dies wollen wir möglichst flexibel und einfach mit einer Service-Discovery lösen. Da auf jedem CoreOS-Host Etcd läuft und dort auch der State des Clusters gespeichert wird, bietet es sich an, Etcd für Service-Discovery zu benutzen. Man könnte beispielsweise ein Datenbank-Image starten, das selbstständig seine IP-Adresse und Port in Etcd einträgt. Die Webapplikation kann diesen Eintrag lesen und somit eine Verbindung zur Datenbank aufbauen. Jedoch möchte man vermutlich nicht jedes Image anpassen, damit es auf der CoreOS-Umgebung läuft. Ein gängiges Muster ist das Ambassador Pattern, welches in einigen Quellen [10], [11] aufgezeigt wird (Abb. 1). Es beschreibt einen Weg, Docker-Links auch über Hosts hinweg aufzubauen.
Für das Ambassador Pattern und andere Dienste erhalten die Services zusätzlich so genannte Sidekick-Container. Das sind Container, die auf dem gleichen Host laufen und z. B. administrative Dinge (wie die Anmeldung in Etcd für den eigentlichen Service) übernehmen. So wird diese Logik aus den Images für die Webapplikation und der Datenbank herausgehalten und bleibt unabhängig von der verwendeten Service-Discovery. Viele Images im Docker-Hub können bereits mit Docker-Links umgehen und sind daher leicht in einem verteilten Kontext mit dem Ambassador Pattern wiederzuverwenden.
Unlock the full potential of Terraform for Infrastructure as Code (IaC). This whitepaper dives into five essential Terraform functions—Lookup, Coalesce, Concat, Element, and Format—that simplify data manipulation and resource management. Learn practical techniques to enhance flexibility, efficiency, and maintainability in your cloud infrastructure. Perfect for DevOps professionals looking to optimize workflows and reduce complexity.
Um etwas konkreter zu werden und die Abhängigkeit zwischen Datenbank und Webapplikation aufzulösen, geben wir zuerst der Datenbank einen Sidekick, welcher überwacht, ob die Datenbank läuft und über welche IP-Adresse und Port sie zu erreichen ist. Dazu bietet sich das Registrator Image [12] von Jeff Lindsay an. Ein Registrator-Container horcht am Docker-Eventstream [13] und trägt automatisch alle Container, die einen Port veröffentlichen, in Etcd ein.
Listing 4: „registrator.service“
[Unit]
Description=Registrator
Service]
Environment="IMAGE=giantswarm/registrator:latest"
EnvironmentFile=/etc/environment
TimeoutStartSec=0
ExecStartPre=-/usr/bin/docker kill %n
ExecStartPre=-/usr/bin/docker rm %n
ExecStartPre=/usr/bin/docker pull $IMAGE
ExecStart=/usr/bin/docker run -v /run/docker.sock:/tmp/docker.sock -h %H --name %n $IMAGE etcd://${COREOS_PRIVATE_IPV4}:4001/services
ExecStop=/usr/bin/docker stop %n
[X-Fleet]
Global=true
In der Servicebeschreibung in Listing 4 gibt es wieder etwas Neues: Wir lesen, dass CoreOS-Environment (/ etc/environment), in dem Environment-Variablen definiert werden, über die man an die IP-Adressen des Hosts (COREOS_PRIVATE_IPV4, COREOS_PUBLIC_IPV4) kommt. Das Environment wird, je nach Hostinganbieter, beim Start des Hosts individuell initialisiert. Der Container bekommt außerdem den Docker-Socket, den Hostnamen des Hosts und einen Pfad für Etcd, wo die Services abgelegt werden können, übergeben.
In der X-Fleet-Sektion der Servicedefinition teilen wir Fleet mit, dass dieser Service auf allen Hosts im Cluster gestartet werden soll. Somit können wir alle Services automatisch in Etcd registrieren: $ fleetctl start registrator.service.
Als Datenbank verwenden wir Redis. Durch die Requires- und After-Anweisungen sorgt Systemd dafür, dass zuerst der Registrator heruntergeladen sowie gestartet wird. Im Anschluss dann der Redis-Container:
Listing 5: „redis.service“
[Unit]
Description=Redis
Requires=registrator.service
After=registrator.service
[Service]
Environment="IMAGE=dockerfile/redis:latest"
EnvironmentFile=/etc/environment
TimeoutStartSec=0
ExecStartPre=-/usr/bin/docker kill %n
ExecStartPre=-/usr/bin/docker rm %n
ExecStartPre=/usr/bin/docker pull $IMAGE
ExecStart=/usr/bin/docker run --rm -p 6379 --name %n $IMAGE
ExecStop=/usr/bin/docker stop %n
Restart=always
$ fleetctl start redis.service
Über das Journal kann man nun sehen, ob der Registrator den Start des neuen Containers mitbekommen hat:
$ fleetctl ssh redis.service 'docker logs registrator.service'
Man kann auch direkt die Daten, die der Registrator geschrieben hat, aus Etcd auslesen:
$ fleetctl ssh redis 'etcdctl ls /services/redis'
Jetzt haben wir eine Datenbank und ein „Announcement“ der Datenbank in Etcd. An der Gegenstelle brauchen wir nun einen Ambassador, der die Anfragen der Webapplikation an den „richtigen“ Redis-Container weiterleitet. Der Ambassador kann ein ganz simpler TCP-Proxy sein oder aber auch (je nach Service) ein Proxy auf einem höheren Layer (z. B. Varnish, MySQL Proxy etc). Wir beschränken uns aber zunächst auf einen einfachen TCP-Proxy und nehmen dafür das ambassadord-Projekt [14] von Jeff Lindsay. Damit die Anwendung sauber starten kann, muss zuerst der Ambassador starten und danach die Webapplikation:
Listing 6: „redis-ambassador.service“
[Unit]
Description=Redis Ambassador
[Service]
Environment="IMAGE=giantswarm/ambassadord:latest"
EnvironmentFile=/etc/environment
TimeoutStartSec=0
ExecStartPre=-/usr/bin/docker kill %n
ExecStartPre=-/usr/bin/docker rm %n
ExecStartPre=/usr/bin/docker pull $IMAGE
ExecStart=/usr/bin/docker run --name %n $IMAGEetcd://${COREOS_PRIVATE_IPV4}:4001/services/redis
ExecStop=/usr/bin/docker stop %n
$ fleetctl start redis-ambassador.service
Der Ambassador sucht jetzt an der angegebenen Stelle in Etcd nach Backends für den Service. Auch hier können wir wieder über das Journal herausfinden, ob der Ambassador das Backend auch wirklich gefunden hat:
$ fleetctl journal -f redis-ambassador
Zuletzt starten wir eine Anwendung, die das aktuelle Kölner Wetter temporär in Redis speichert und per HTTP ausgibt. Der Container der Anwendung muss deshalb mit dem Redis Ambassador gelinkt werden. Dadurch werden Environmentvariablen im Anwendungscontainer definiert, über die die Anwendung dann auf den Redis zugreifen kann [15]. Die Environmentvariablen bestehen aus dem zweiten Teil des Links hinter dem Doppelpunkt und dem Port, auf den man zugreifen möchte. ambassadord erwartet, wenn er Etcd als Backend verwendet, Verbindungen von Clients über Port 10000. Wichtig ist auch hier, dass die Anwendung auf dem gleichen Host wie der ambassadord gestartet wird, da sonst die beiden Container nicht verlinkt werden können:
Listing 7: „weather.service“
[Unit]
Description=Weather application
Requires=redis-ambassador.service
After=redis-ambassador.service
[Service]
Environment="IMAGE=teemow/currentweather:latest"
TimeoutStartSec=0
ExecStartPre=-/usr/bin/docker kill %n
ExecStartPre=-/usr/bin/docker rm %n
ExecStartPre=/usr/bin/docker pull $IMAGE
ExecStart=/usr/bin/docker run --link redis-ambassador.service:redis -p 1337 --name %n $IMAGE
ExecStop=/usr/bin/docker stop %n
[X-Fleet]
MachineOf=redis-ambassador.service
$ fleetctl start weather.service
Ist der Container mit der Anwendung erfolgreich heruntergeladen, kann mit Curl eine Anfrage zum derzeitigen Kölner Wetter gestellt werden:
$ fleetctl ssh weather.service 'curl $(docker inspect --format "{{ .NetworkSettings.IPAddress }}" weather.service):1337'
Der gleiche Wert sollte auch in Redis zu finden sein:
$ fleetctl ssh redis.service 'docker run --rm -ti dockerfile/redis redis-cli -h $(docker inspect --format "{{ .NetworkSettings.IPAddress }}" redis.service) get currentweather'
Dieses Setup erfordert natürlich noch eine Menge Handarbeit, und man verliert auch schnell den Überblick über Abhängigkeiten in den Servicedefinitionen. Es empfiehlt sich, das Fleet-API [16] zu benutzen und die eigenen Deployments zu automatisieren. Das Tooling rund um Fleet ist leider noch etwas rar, aber das API sollte sich mit den nächsten Releases aus dem „Experimental“-Status heraus begeben, sodass man durchaus damit arbeiten kann. Wie immer gilt auch hier: Know your Tools.
Der größte Bruch im Design von CoreOS ist leider immer noch die fehlende Integration von Docker und Systemd. Das liegt vor allen Dingen daran, dass man im Systemd-Service den Docker-Client aufruft, der dann mit dem Docker Daemon spricht. In Systemd wird demzufolge nicht der Container selbst, sondern nur der Docker-Clientprozess gemanagt. Diese Probleme dürften sich aber in naher Zukunft erledigen, und man darf gespannt bleiben, was die nächsten Releases bringen.
CoreOS steckt noch in den Kinderschuhen, aber Konzepte wie automatische Updates, Container und konsistenter Cluster-State entsprechen einem modernen Linux und versprechen eine spannende Zukunft. Die Eigenentwicklung von Etcd und Fleet sind dabei genauso wichtig wie die Integration von Systemd und Docker. CoreOS zeigt auf eine sehr praktikable Art und Weise, wie diese Tools ineinandergreifen und dass ein solches System gleichzeitig aufgeräumt, strukturiert sowie mächtig sein kann. Die Möglichkeiten für verteilten Storage und Software-Defined-Networking sind hier noch nicht ausreichend berücksichtigt.
Timo Derstappen ist Mitgründer von Giant Swarm in Köln. Er hat langjährige Erfahrung mit dem Aufbau von skalierbaren und automatisierten Cloud-Architekturen, und sein Interesse wird meist von leichtgewichtigen Konzepten bzgl. Produkt-, Prozess- und Softwareentwicklung geweckt. Freie Software ist für ihn ein Grundprinzip.
[1] https://github.com/coreos/etcd
[2] https://github.com/coreos/fleet
[3] https://discovery.etcd.io/
[4] https://coreos.com/docs/running-coreos/cloud-providers/ec2/
[5] https://github.com/coreos/coreos-vagrant
[6] https://coreos.com/docs/running-coreos/cloud-providers/digitalocean/
[7] https://github.com/coreos/fleet/releases
[8] https://github.com/giantswarm/ping
[9] https://coreos.com/docs/launching-containers/launching/launching-containers-fleet/
[10] https://docs.docker.com/articles/ambassador_pattern_linking/
[11] https://coreos.com/blog/docker-dynamic-ambassador-powered-by-etcd/
[12] https://github.com/progrium/registrator
[13] https://docs.docker.com/reference/api/docker_remote_api_v1.14/#monitor-dockers-events
[14] https://github.com/progrium/ambassadord
[15] https://github.com/teemow/currentweather/blob/master/server.js#L4
[16] https://github.com/coreos/fleet/blob/master/Documentation/api-v1-alpha.md