12

I have a consul cluster of 3 servers. I also have a docker swarm of around 6 workers and 3 masters (the masters are on the same hardware as the consul servers but are set with availability == drain to prevent them accepting work).

I generally use consul-template to read consul K/V. I cannot for the life of me work out how to sensibly roll out a consul agent service. If I use a global service then I get one agent per node but the server cluster complains because the client agents all appear to have the same IP address.

Replicated services seem to be the way to go, but I believe I need to publish the client port 8301 and that seems to cause a clash with my server cluster (which is running both swarm master and consul servers (not under docker).

I'd appreciate a general steer in the right direction - bearing in mind this is 1.12 swarm mode and therefore very different from earlier versions.

sendmoreinfo
  • 582
  • 6
  • 22
MarkH
  • 682
  • 1
  • 6
  • 16

4 Answers4

6

It's confusing but Docker "Swarm Mode" is really a different animal that what is still called Docker Swarm. In Swarm Mode you don't need Consul. The docker daemon on each host acts as the key value store and does the service discovery. It does everything for what Consul is needed in the "old" Docker Swarm.

Just be careful to look for documentation/info that is specific to "swarm mode" only. I wish they had used a different name for it actually.

Bernard
  • 16,149
  • 12
  • 63
  • 66
  • Really? Are you saying that I can somehow point consul-template to read my application values ? I know that I don't need it to implement swarm features, but we use it for much more that t6hat – MarkH Aug 25 '16 at 10:18
  • Typically consul-template is used to update the config for nginx or haproxy. It basically adds or removes ip addresses on the list of servers to load balance. This is not needed in Swarm Mode. You let docker do the load balancing. It's good to have nginx (for instance) fronting your service to handle things like SSL, gzip, security but its config file can simply use the dns name "myservice" for upstream server. If you've created a docker service named "myservice", docker will do the load balancing to the proper container. – Bernard Aug 25 '16 at 13:29
  • We use consull-template to populate files for a whole bunch of legacy apps - I'm struggling to see how this can be achieved with just swarm. I undersatnd how swarm now implements much of the service discovery and load balancing that used to rely on third party tools. But as a general purpose K/V store I just can't see how to use it. – MarkH Aug 25 '16 at 16:37
  • @MarkH I see what you mean. In sort, I don't think it's possible with Docker 1.12. Docker 1.13 might add the changes needed. The longer story: The new Swarm Mode uses an internal kv store (based on etcd) but it's only for its own use. Moreover, there's no way to listen to events about containers/services being started/stopped. That's a feature slated for Docker 1.13 though https://github.com/docker/docker/issues/23827. Registrator needs this to update Consul. If you really need something now, you could use polling using the Docker Remote API but that's like writing your own Registrator. – Bernard Aug 26 '16 at 14:05
  • I'm using consul as key-value store for my own microservices and this has nothing to do with hostnames at all. Also traefik is using it. – holms Jun 12 '18 at 14:34
  • I'm using consul as key-value store for my own microservices and this has nothing to do with hostnames at all. Also traefik is using it. – holms Jun 12 '18 at 14:35
6

After much deliberation and many dead ends, we finally came up with a solution that works for us. Part of the problem is that at the time of writing, Docker 1.12 is somewhat juvenile and introduces a number of concepts that have to be understood before it all makes sense. In our case, our previous experiences with pre 1.12 variants of Swarm have hindered our forward thinking rather than helped.

The solution we utilised to deploy a consul K/V service for our swarm goes as follows

  1. Create an overlay network called 'consul'. This creates an address space for our service to operate within.

    docker network create --driver overlay --subnet 10.10.10.0/24 consul

  2. Deploy the consul server cluster into the new overlay. We have three hosts that we use as manager nodes and we wanted the consul server containers to run on this cluster rather than the app servers hence the 'constraint' flag

    docker service create -e 'CONSUL_LOCAL_CONFIG={"leave_on_terminate": true}' --name consulserver --network consul --constraint 'node.role == manager' --replicas 3 consul agent server -bootstrap-expect=3 -bind=0.0.0.0 -retry-join="10.10.10.2" -data-dir=/tmp

    The key here is that swarm will allocate a new VIP (10.10.10.2) at the start of the consul network that maps onto the three new instances.

  3. Next we deployed an agent service

    docker service create \ -e 'CONSUL_BIND_INTERFACE=eth0' \ -e 'CONSUL_LOCAL_CONFIG={"leave_on_terminate": true, "retry_join":["10.10.10.2"]}' \ --publish "8500:8500" \ --replicas 1 \ --network consul \ --name consulagent \ --constraint 'node.role != manager' \ consul agent -data-dir=/tmp -client 0.0.0.0

Specifying the VIP of the consulserver service. (Consul won't resolve names for join - other containers may do better, allowing the service name "consulserver" to be specified rather than the VIP)

This done, any other service can access the consulagent by being joined to the consul network, and resolving the name "consulagent". The consulagent service can be scaled (or maybe deployed as a global service) as required. Publishing port 8500 makes the service available at the edge of the swarm and could be dropped if you didnt need to make it available to non swarm services.

MarkH
  • 682
  • 1
  • 6
  • 16
  • Same question as I replied to @electrometro's answer: isn't publishing the Consul API port dangerous? – joonas.fi Oct 11 '16 at 15:15
  • Publish the port to whom? Our entire network is effectively a DMZ. Only devices on the same VLAN have access to any of these services so the risk is minimal. I'm curious how else you would do it - we're always open to ways we can improve our deployment. – MarkH Oct 24 '16 at 16:35
  • 1
    Before Docker Swarm mode, I bound Consul API to the bridge interface IP so the API was accessible from within the cluster but not to the outside world. That was the only option as Consul was advised to be ran on the host network namespace. I'm not sure if my solution was sensible to do but that's exactly the reason I came here to research, trying to find better ways.. :) – joonas.fi Oct 25 '16 at 10:23
  • Not sure of the cause, but just a warning - creating the service caused 2 out of 3 docker swarm manager nodes to crash and not recover with error on the docker service daemon: `Error starting daemon: layer does not exist` – Brandon Mar 31 '17 at 17:44
3

In my blog I explore a similar way to MarkH's answer but the key difference is that instead of pointing to the VIP of the new servers, I am pointing to the first three nodes that join the network. This can be beneficial due to the VIP has issues where it will point to itself versus load balancing that across all the nodes on that VIP. In my experience it was better to do it this way for the service creation.

docker service create \
  --network=consul \
  --name=consul \
  -e 'CONSUL_LOCAL_CONFIG={"skip_leave_on_interrupt": true}' \ 
  -e CONSUL_BIND_INTERFACE='eth0' \
  --mode global \
  -p 8500:8500 \ 
  consul agent -server -ui -client=0.0.0.0 \
  -bootstrap-expect 3 \
  -retry-join 172.20.0.3 \
  -retry-join 172.20.0.4 \
  -retry-join 172.20.0.5 \
  -retry-interval 5s

I am using global mode here in a 3 node swarm so you can swap that out for replicas and put your constraints.

Jared Mackey
  • 3,998
  • 4
  • 31
  • 50
  • Originally we were using the same retry-join set as this. However using the VIP just seemed cleaner. The load balancer isn't involved once the cluster is established as the gossip protocol will propagate the individual cluster member IP's rather than the VIP. I wish we'd located your blog when we were researching... – MarkH Aug 31 '16 at 08:41
  • @MarkH, me too! We spent days coming up with this. :). That is very interesting about the VIP though, we may need to do some more research around how those are working. – Jared Mackey Sep 12 '16 at 15:52
  • 1
    Isn't publishing the agent's port dangerous? You don't want the Consul API to be accessible to the internet.. I guess you could block the internet via firewall rules but I guess that kinda defeats the networking model of Docker (private vs. published ports) – joonas.fi Oct 11 '16 at 15:14
  • Yeah it would be dangerous but we are behind a firewall. I'm not sure how else to access those without publishing them. – Jared Mackey Oct 11 '16 at 15:15
  • Compose version please :)? What are those IP's in there, I can't tight setup to any ip, it should be dynamic – holms Jun 12 '18 at 14:32
3

For those like me that prefer to run our services from docker-compose.yml files, I managed to "docker stack deploy"

https://github.com/thechane/consul/blob/master/docker-compose.yml

... to run Consul as a Docker service.

--- EDIT , poor form to just answer with links so here it is:

version: '3.1'
#customise this with options from
#https://www.consul.io/docs/agent/options.html

services:

seed:
  hostname: seed
  image: consul:0.8.0
  deploy:
    restart_policy:
      condition: none  #we do not want this to be restarted on timeout (see entrypoint options below)
    replicas: 1
    placement:
      constraints:
        - "engine.labels.access == temp"
        - "engine.labels.access != consul"
  environment:
    - "CONSUL_LOCAL_CONFIG={\"disable_update_check\": true}"
    - "CONSUL_BIND_INTERFACE=eth0"
  entrypoint:
    - timeout     #this seed fires up the cluster after which it is no longer needed
    - -sTERM      #this is the same signal as docker would send on a scale down / stop
    - -t300       #terminate after 5 mins
    - consul
    - agent
    - -server
    - -bootstrap-expect=5
    - -data-dir=/tmp/consuldata
    - -bind={{ GetInterfaceIP "eth0" }}
  networks:
    - "consul"

cluster:
  image: consul:0.8.0
  depends_on:
    - "seed"
  deploy:
    mode: global                                      ##this will deploy to all nodes that
    placement:
      constraints:
        - "engine.labels.access == consul"            ##have the consul label
        - "engine.labels.access != temp"
  environment:
    - "CONSUL_LOCAL_CONFIG={\"disable_update_check\": true}"
    - "CONSUL_BIND_INTERFACE=eth0"
    - "CONSUL_HTTP_ADDR=0.0.0.0"
  entrypoint:
    - consul
    - agent
    - -server
    - -data-dir=/tmp/consuldata
    - -bind={{ GetInterfaceIP "eth0" }}
    - -client=0.0.0.0
    - -retry-join=seed:8301
    - -ui                                              ##assuming you want the UI on
  networks:
    - "consul"
  ports:
    - "8500:8500"
    - "8600:8600"

networks:
  consul:
    driver: overlay

Also note, I later discovered that without the seed more consul instances can not be added. So if you intend to expand your swarm node count I'd remove the timeout command with its options from the seed entrypoint.

thechane
  • 349
  • 1
  • 2
  • 12
  • I haven't revisited this deployment for a while - it's been doing it's job .. However, your solution is very cool. Can you explain a couple of aspects that aren't clear to me 1. Can you provide a reference to the engine.labels.access that controls deployment - I can't find it anywhere and the only vague ref. I have is for Docker UCP (which we don't use) 2. What is the 'seed's job - why not just launch the cluster directly? – MarkH Apr 17 '17 at 16:42
  • The seed I found necessary to bootstrap the cluster, once it's all running the seed stops and is no longer needed. Somewhere in the Consul docs it says that only one node can use the bootstrap option. For the labeling check out - https://docs.docker.com/engine/reference/commandline/node_update/#examples – thechane Apr 19 '17 at 18:39
  • It's still not clear to me how your labels work. In the compose file you have placement constraints as follows `constraints: - "engine.labels.access == temp" - "engine.labels.access != consul" ` The constraints appear to be mutually exclusive ( 'temp' can **never** equal 'consul' ). Hence my question. – MarkH Apr 21 '17 at 09:25
  • Labels are set up however you wish, this is just an imaginary example where I have some nodes labeled with temp, some with consul and some with both. I want the service to be scheduled onto any node that has the temp label so long as it does not also have the consul label. – thechane May 03 '17 at 20:22
  • What is this {{ GetInterfaceIP ... }} stuff in the compose file? – James Mills Jun 07 '17 at 06:39
  • 1
    It's a poorly documented Consul feature. https://godoc.org/github.com/hashicorp/go-sockaddr/template https://github.com/hashicorp/consul/issues/2217 – thechane Jun 10 '17 at 14:58
  • I've been trying to work off of this solution, but there seems to be an issue if the seed goes down. When docker brings it back up it needs to rejoin the cluster, so `-retry-join=cluster:8301` is necessary. But, if another server also restarted at the same time, the seed could end up joining with that instead of one that's already part of the cluster (the load-balancer doesn't know who's on the cluster). Then neither node would join the cluster, and just sit there deadlocked. I think that the solution offered by @MarkH also suffers from a similar problem. Perhaps I'm missing something? – btidwell Sep 28 '17 at 18:49