Erstellen einer lokalen Entwicklungsumgebung mit CoreDNS, SSH und Docker Swarm

September 3, 2017 by Dimi

Other languages: en

Setup overview

Abbildung 1: Abstrakte Darstellung des Aufbaus

Die Arbeit mit Docker in einer lokalen Umgebung ist großartig und kann eine Menge Dinge einfacher machen. Mit Swarm kann man sogar viele Maschinen zu einer “gemeinsamen” zusammenfassen und somit alle Ressourcen einfach nutzen.

Es gibt dabei viele Möglichkeiten, wie beispielsweise das Einrichten eines Swarm-Clusters auf AWS oder Digital Ocean und daraufhin das Bereitstellen verschiedener Dienste. Klingt spaßig.

Allerdings ist es ein bisschen ärgerlich, immer wieder ein paar Sachen in die etc/hosts schreiben zu müssen. Vor allem, wenn man verschiedenste Dienste über Swarm bereitstellt. Viel zu viele. Auf unterschiedlichen Clustern noch dazu… Oje…
Die andere Sache dabei ist, dass es keine gute Idee ist, die Dienste auch noch der weiten Welt Bereitstellen zu lassen, vor allem beim Experimentieren oder Üben.

Wie kann man also komfortabel arbeiten, z.B. mit mehreren Swarm-Clustern von einem lokalen Rechner aus, ohne sich um all die verschiedenen Konfigurationen kümmern zu müssen, die widerum nötig sind, um die Dienste dementsprechend abzusichern und auf diese einfach zugreifen zu können, z. B. unter myservice.some.domain?

Abbildung 1 zeigt die vereinfachte Darstellung eines möglichen Setups. Sehr schnell, sicher und ohne komplizierte Konfigurationen. Gewöhnlich hat man SSH-Zugriff auf entfernte Maschinen. Dies sollte ausreichend sein, um damit arbeiten zu können und ohne zusätzlich viele Ports öffnen zu müssen. Man kann entweder einen lokalen Port an den entfernten Rechner weiterleiten oder sogar VPN-over-SSH verwenden, z. B. Shuttle.

Ich arbeite die meiste Zeit mit WebDev, sodass die Verwendung von HTTP/S für meine Aufgaben ausreichend ist. Hier sieht man ein Beispiel einer Portweiterleitung (lokal 80 nach remote 80).

$ ssh -L 80:123.123.123.123:80 user@123.123.123.123 -N

So kann man nun auf einen Web-Service auf dem entfernten Rechner zugreifen, indem man einfach localhost im Browser eintippt. Das ist wirklich praktisch, wenn man mit nur einem entfernten Rechner arbeiten möchten. Und es läuft sonst nichts lokal auf Port 80. Hmmm.

Falls nicht, kann man ja sshuttle (wie oben vorgeschlagen) verwenden. So erhält man hierdurch

$ sshuttle -r user@123.123.123.123 123.123.123.0/24

die Möglichkeit, über die Remote-IP (123.123.123.123) auf Port 80 auf den entferneten Web-Service zuzugreifen. Am bequemsten ist es natürlich, wenn man eine Zeile 123.123.123.123.123.123 remote.server in die Hosts-Datei einträgt und eine URL anstelle der IP-Adresse eingibt (remote.server). Hmmm.
Und was ist, wenn man mehrere Dienste wie z.B. webapp1 und webapp2 auf der Remote-Maschine laufen hat? (Load-Balancing und Subdomain-Routing werden später besprochen)

Klar ist, dass man für jeden Dienst (wenn man über eine Subdomain, wie z. B. webapp1. remote. server, auf den Dienst zugreifen möchte) eine weitere Zeile in die Hosts-Datei einfügen müsste. Könnte wirklich unbequem werden.
. Natürlich kann man über Pfade wie /myblog --> webapp1 oder /mysite --> webapp2 auf die verschiedenen Dienste zugreifen. Ich persönlich bevorzuge aber einen anderen Ansatz.

Eine gute Idee wäre es, einen DNS-Server lokal laufen zu lassen. Ein paar von euch bekommen bestimmt einen Schreck vor so einer Idee, aber es gibt einige wirklich sehr sehr einfache Lösungen dafür, nämlich etwa CoreDNS und Docker. Verwendung von CoreDNS mit Docker und dabei keine Kopfschmerzen!
Natürlich muss man noch etwas an Konfiguration hinzufügen, aber es ist trotzdem noch recht einfach und überschaubar.

Man kann den lokalen DNS mit Docker folgendermaßen starten:

$ docker run --rm -p 53:53/udp -v $(pwd):/coredns-config/ coredns/coredns -conf /coredns-config/Corefile

Hier wird der Container beim Beenden gelöscht, der Container-Port 53 auf den lokalen Port 53 gebunden und die gesamte Konfiguration ist dabei in der Corefile definiert.

Der Inhalt der Corefile ist recht einfach.

.:53 {
  file /coredns-config/docker.swarm docker.swarm
  proxy . 8.8.8.8:53 [2001:4860:4860::8888]:53
  prometheus
  errors stdout
  log stdout
}

Also werden alle andere Anfragen, außer docker.swarm, an einen Google-DNS weitergeleitet. Für die docker.swarm Domain wird die entsprechende Konfiguraion verwendet (/coresns-config/docker.swarm).

Der Inhalt hier ist auch ganz einfach:

$TTL 60
$ORIGIN docker.swarm.
@                   IN	SOA sns.dns.icann.org. noc.dns.icann.org. (
          2017042745 ; serial
          7200       ; refresh (2 hours)				
          3600       ; retry (1 hour)			
          1209600    ; expire (2 weeks)				
          3600       ; minimum (1 hour)				
          )
@                   IN A     123.123.123.123
*.docker.swarm.     IN A     123.123.123.123

Jede Subdomain und docker.swarm verweist auf die Remote-IP (bzw. den Swarm-Manager). Im Grunde genommen ist es ganz einfach.

Wenn man also den Container startet, kann man mit dig testen, ob das auch wirklich funktioniert:

$ dig docker.swarm                 

; <<>> DiG 9.8.3-P1 <<>> docker.swarm
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 20479
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;docker.swarm.			IN	A

;; ANSWER SECTION:
docker.swarm.		60	IN	A	123.123.123.123

;; Query time: 47 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Fri Sep  1 23:06:42 2017
;; MSG SIZE  rcvd: 46

Dasselbe erhält man auch, wenn man auf eine Subdomain zugreift. Man sollte zudem nicht vergessen resolv.conf (hinzufügen von nameserver 127.0.0.0.1) zu aktualisieren, damit die lokale Maschine den lokalen DNS verwenden kann.

Damit ist auch der nächste große Teil gelöst. Wenn man nun eine Webapp auf einen Swarm-Cluster bereitstellt, kann man einfach über die Domain (docker.swarm) darauf zugreifen.

Das nächste Problem betrifft also die Ports.
Wenn man einen Container startet und einen Port an den Host bindet (z.B. Host 80 und Container 80) kann man keinen weiteren Container mehr an denselben Port binden. D.h. man muss auf eine andere Webapp auf einem anderen Port zugreifen (z. B. docker.swarm: 8080 statt einfach nur 80). Das ist widerum sehr unbequem.

Hier kommt der Punkt, an dem man einen guten Load-Balancer, wie z.B. traefik, brauchen könnte.

An dieser Stelle kann man nochmal einen Blick auf Abbildung 1 werfen. Traefik wird die Anfragen (z.B. jupyter.docker.swarm oder sharelatex.docker.swarm) an den entsprechenden Container weiterleiten. Man kann auch nur Subdomains verwenden. Keine Ports. Keine Kopfschmerzen.

Ich habe eine docker-stack. yml für traefik verwendet. So sieht sie aus:

version: '3'
services:
  traefik:
    image: traefik:latest
    command: >
      --web --docker --docker.swarmmode
      --docker.domain=docker.swarm --docker.watch
      --accesslogsfile=/dev/stdout --logLevel=DEBUG
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /dev/null:/traefik.toml
    ports:
      - "80:80"
      - "8080:8080"
    deploy:
      replicas: 1
      restart_policy:
        condition: on-failure
      placement:
        constraints:
          - node.role == manager
      labels:
        - "traefik.port=8080"
        - "traefik.backend=traefik"
        - "traefik.frontend.rule=Host:traefik.docker.swarm"
        - "traefik.docker.network=traefik_default"

Man beachte, dass die Domain im Command-Bereich angegeben wurde. Als nächstes wurde der Container auf dem Manager-Knoten platziert (CoreDNS-Container löst die Domain auf die Manager-IP auf) und einige Labels angegeben. Das traefik-Dashboard ist unter traefik. docker. swarm erreichbar. Die Stack-Konfig kann man mit Docker folgendermaßen verwenden:

docker stack deploy -c docker-cloud.yml traefik

Anmerkung: Wenn man traefik auf diese Weise startet, wird ein Standard-Netzwerk erstellt (traefik_default). Wenn man also andere Dienste bereitstellen möchte, sollte man traefik_default zum Dienst hinzufügen (als externes Netzwerk) und das entsprechende Label angeben (wie oben). Macht man dies nicht, wird man wahrscheinlich ein Timeout erhalten, da Traefik nicht in der Lage sein wird, die Anfragen weiterzuleiten.

Man kann das gesamte Setup nun testen, indem man einen einfachen whoami Container laufen lässt, der Informationen, wie z.B. “wohin die Anfrage gegangen ist” liefert (Container-ID).

Ich habe einen Swarm-Cluster bestehend aus drei Workern und einem Manager:

$ docker node ls
ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS
4v4pbtu27a8znsan86jf3uacr     docker-4            Ready               Active              
kzanvv77u4v0ptk9p3zcvlzue     docker-3            Ready               Active              
mhs4e6yw9io8pw9tnoxbgwcmq *   docker-1            Ready               Active              Leader
q07x2s9r640ia3vfkj75fhhzo     docker-2            Ready               Active  

Mit docker service create --name whoami --network traefik_default --label traefik.port=80 --replicas 4 emilevauge/whoami sollte man somit jeweils einen Container pro Knoten bekommen.

Hier ein paar weitere Details:

$ docker service ps whoami 
ID                  NAME                IMAGE                      NODE                DESIRED STATE       CURRENT STATE       
oiwp51fs9f4p        whoami.1            emilevauge/whoami:latest   docker-3            Running             Running 19 hours ago                       
rxr0w0ix7c9l        whoami.2            emilevauge/whoami:latest   docker-4            Running             Running 19 hours ago                       
kl1lmnfux6e6        whoami.3            emilevauge/whoami:latest   docker-2            Running             Running 19 hours ago                       
d7af62xugy4o        whoami.4            emilevauge/whoami:latest   docker-1            Running             Running 19 hours ago 

Mit curl kann man sehen, dass Anfragen zu jedem Container mit Round Robin weitergeleitet werden (man kann alles mit traefik konfigurieren):

(first request)

$ curl whoami.docker.swarm
Hostname: 2c049cd20104
IP: 127.0.0.1
IP: 10.0.1.16
IP: 10.0.1.5
IP: 172.18.0.8
GET / HTTP/1.1
Host: whoami.docker.swarm
User-Agent: curl/7.54.0
Accept: */*
Accept-Encoding: gzip
X-Forwarded-For: 10.255.0.2
X-Forwarded-Host: whoami.docker.swarm
X-Forwarded-Port: 80
X-Forwarded-Proto: http
X-Forwarded-Server: 4c8275a2a00c
X-Traefik-Reqid: 9912

(second request)

$ curl whoami.docker.swarm
Hostname: df827b31f25c
IP: 127.0.0.1
IP: 10.0.1.15
IP: 10.0.1.5
IP: 172.18.0.4
GET / HTTP/1.1
Host: whoami.docker.swarm
User-Agent: curl/7.54.0
Accept: */*
Accept-Encoding: gzip
X-Forwarded-For: 10.255.0.2
X-Forwarded-Host: whoami.docker.swarm
X-Forwarded-Port: 80
X-Forwarded-Proto: http
X-Forwarded-Server: 4c8275a2a00c
X-Traefik-Reqid: 10014

(Der oben angeführte Hostname ist offensichtlich nicht die ID der ausgegebenen Serviceaufgaben, sondern nur die Container-ID.)

Wenn hier Fragen oder Verbesserungsvorschläge entstehen, höre ich mir diese gerne an :)

Copyright © 2018 Dimitrij Klesev | Hucore theme & Hugo