Setup a local devenv with CoreDNS, SSH and Docker Swarm

September 3, 2017 by Dimi

Other languages: de

Setup overview

Image 1: Abstract overview of the setup

Working with docker in a local environment is great and can make a lot of dev things easier. With swarm you can even lump together all your machines and use all the ressource as one big view.

There are a lot of possibilities, like setting up a swarm cluster on AWS or Digital Ocean in minutes and then deploy your services. This is straightforward.

However adding some stuff to your /etc/hosts everytime is a bit annoying. Especially if you deploy different services to swarm. A lot of different services. On different swarms.. Overcomplicated..
Also, opening up everything or even a bit of everything to the world wide web is bad practice. Especially while experimenting. Not a good idea.

So how can one work in a comfortable way with e.g. multiple swarm clusters from a local machine and don’t care about all the configuration needed to secure the services and access them easily, e.g. under myservice.some.domain?

Image 1 shows a simplified setup of what I like to use. Very quick, secure and without annoying things. Usually one has SSH access to the machines. This should be sufficient to work with. Without opening any ports. One can either forward some local ports to the remote machine or even use VPN-over-SSH, e.g. sshuttle.

Most of the time I work with WebDev so using HTTP/S is sufficient for my tasks. Here you see a common example of port forwarding (local 80 to remote 80).

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

So now I can simply access a web-service on the remote machine by just typing localhost into my browser. It’s really handy if you want to work with only one remote machine, but it will use port 80 locally. Hmmm.

If you don’t want it to occupy port 80 locally, you can just use sshuttle (as suggested above). So by running

$ sshuttle -r user@123.123.123.123 123.123.123.0/24

one gets the possibility to access the remote web-service (e.g.) on port 80 by just using the remote IP (123.123.123.123). Surely the more comfortable way is to add something like 123.123.123.123 some.domain to the remote server’s hosts file and then typing this url instead of the IP. Hmmm.
And what is if I have multiple services like webapp1 and webapp2 running on the remote machine? (Load balancing and subdomain routing will be discussed later)

Normally, we would have to add another line to the hosts file for every service (if we want to access them via a subdomain, like webapp1.remote.server). Could become really annoying.. Sure we could use some path-routing like /myblog --> webapp1 or /mysite --> webapp2. I don’t like such crap.

A much easier solution is to run some DNS server locally. A lot of people are scared of such ideas but there are some really really simple solutions for this. Like CoreDNS. And docker. Running CoreDNS with docker. No headache. Of course we have to add some configuration (but it’s still quite simple though..)

So I run my local DNS this way:

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

Here the container gets deleted on exit, port binding is simply set to dns and I provide some configurations (like Corefile).

The content of the Corefile is quite simple.

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

It basically redirects everything except docker.swarm queries to a google dns. For docker.swarm domains it uses another config (/coredns-config/docker.swarm).

The content of this is quite simple, too:

$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

Every subdomain and docker.swarm points to the remote server’s IP. Very simple.

While running the container one can test with dig if it really works:

$ 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

You get the same by accessing a subdomain. To let your local machine use the local dns don’t forget to update resolv.conf (adding nameserver 127.0.0.1).

So now that big part is solved, too. If I deploy a simple webapp to my swarm cluster now, I can simply access it via the domain served by the CoreDNS container (docker.swarm).

The next problem is related to the ports.
If you run a container and bind some ports (e.g. host’s 8080 to container’s 80) you can’t bind another container to the same port. I.e. we have to access another webapp on a different port (e.g. docker.swarm:8080 rather than simply 80). This is somehow annoying.

To solve this we could use a good load balancer like traefik.

Please have a look at the Image 1 at the beginning of this post. Traefik will route our requests (e.g. jupyter.docker.swarm or sharelatex.docker.swarm) to the corresponding container. We only use subdomains. No ports. No headache.

I used a docker-stack.yml for traefik. This is how it looks like:

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"

Note that I provided the domain name in the command section. Next I placed the container on the manager node (dns container will resolve to the manager IP) and some labels. E.g. traefiks dashboard is accessible under traefik.docker.swarm. To deploy the stack simply run:

docker stack deploy -c docker-cloud.yml traefik

Some addtional notes: if you deploy traefik this way, a default network will be created (traefik_default). So when deploying other services you should add traefik_default to the service (as external network) and specify the label (like above). If you don’t do it you’ll probably get a timeout error as traefik will not be able to redirect the request.

We can test the whole setup by running a simple whoami container which will provide information like where the request has been redirected to (container-id) and others.

So I have a swarm cluster consisting of three worker nodes and one 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  

With docker service create --name whoami --network traefik_default --label traefik.port=80 --replicas 4 emilevauge/whoami I shoud get one container running on each node.

So here some 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 

Using curl we can see the requests are balanced to each container with round robin (everything can be configured with traefik):

(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

(Obviously the hostname listed above is not the ID from the service task’s output, it’s just the container ID)

So you have reached the last line - if you have any questions or suggestions, let me know :)

Copyright © 2018 Dimitrij Klesev | Hucore theme & Hugo