How does it work? Docker! Episode 4 - Control your Swarm!

- 18 mins

Code, please!

Yay, code!

First of all, we will need the actual virtual machines where our cluster will run. We will create these using Vagrant. If you don’t know what Vagrant is, or why are we using it, check out this article.

For this deployment, we will have three Manager nodes, and three worker nodes. Why three Manager nodes you say? It may seem overkill, but in order to have High Availability you need to have an odd number of Manager nodes, otherwise, you will not get consensus from Raft. We will see this in action later on.

We do not need an etcd cluster, since the key-value datastore is already included in the internals of the Docker Engine, and therefore, we will not include any machines for it.

First of all, I’ll describe the amount of machines I want and their configurations as variables:

# General cluster configuration
$swarm_manager_instances = 3
$swarm_manager_instance_memory = 2048
$swarm_manager_instance_cpus = 1
$swarm_worker_instances = 3
$swarm_worker_instance_memory = 2048
$swarm_worker_instance_cpus = 1

Afterwards, I’ll specify that I want to use the CoreOs (Contaner Linux) Vagrant box, from an URL:

# Box management: CoreOS
config.vm.box = "coreos-stable"
config.vm.box_url = "https://storage.googleapis.com/stable.release.core-os.net/amd64-usr/current/coreos_production_vagrant.json"

Just a little reminder, Container Linux is a lightweight Linux distribution that uses container to run applications. It ships with basic GNU utilities so you can do all your business, and some other interesting things, like kubelet, Docker, etcd and flannel. We’ll only be using Docker for this part. In short, CoreOS’s Container Linux is an OS specially designed to run containers, and we’re going to profit from that in our context. If you wanna know more about CoreOS, check this article.

We’ll configure our Manager nodes with the variables we previously defined:

# Swarm manager instances configuration
(1..$swarm_manager_instances).each do |i|
  config.vm.define vm_name = "swarm-manager-%02d" % i do |master|
    # Name
    master.vm.hostname = vm_name

    # RAM, CPU
    master.vm.provider :virtualbox do |vb|
      vb.gui = false
      vb.memory = $swarm_manager_instance_memory
      vb.cpus = $swarm_manager_instance_cpus
    end

    # IP
    master.vm.network :private_network, ip: "10.0.0.#{i+110}"
  end
end

And then we’ll do the same thing with our Worker nodes:

# Swarm worker instances configuration
(1..$swarm_worker_instances).each do |i|
  config.vm.define vm_name = "swarm-worker-%02d" % i do |worker|
    # Name
    worker.vm.hostname = vm_name

    # RAM, CPU
    worker.vm.provider :virtualbox do |vb|
      vb.gui = false
      vb.memory = $swarm_worker_instance_memory
      vb.cpus = $swarm_worker_instance_cpus
    end

    # IP
    worker.vm.network :private_network, ip: "10.0.0.#{i+120}"
  end
end

Easy. If you wanna take a look at the Vagrantfile, it’s right here. Moving on.

Next up, I’ll set up a Makefile with a Vagrant target, which will create all the virtual machines, and generate a configuration file with the SSH configuration needed to interact with them. Ill also add a clean target, which will destroy all the machines, and it will delete the SSH configuration file, if it exists.

.PHONY: vagrant clean

vagrant:
    "@vagrant up"
    @vagrant ssh-config > ssh.config

clean:
    @vagrant destroy -f
    @[ ! -f ssh.config ] || rm ssh.config

I’ll also put in a list of phony targets, since the targets previously defined are just actions and don’t really generate files.

We’ve got the virtual machines, and the SSH configuration. For the Ansible part, we’ll start by creating an inventory with all our six nodes in it, separated by groups:

[swarm-leader]
swarm-manager-01

[swarm-manager]
swarm-manager-01
swarm-manager-02
swarm-manager-03

[swarm-worker]
swarm-worker-01
swarm-worker-02
swarm-worker-03

You will probably notice there is a swarm-leader group in the inventory, which contains a single host. Like I said in the first article, there might be many Managers in a cluster; Nevertheless, there is only one Leader at any given moment. We will use this group to launch specific actions for the Leader, common actions for all Manager nodes using the swarm-manager group, and actions destined for the non-Leader Manager nodes using the swarm-manager group, and subtracting the swarm-leader group from it. This may seem complex, but it is actually super easy, you will see.

No IP configurations over here, we’ll just use the SSH configuration file we generated earlier. In order to do that, we have to specify it on our ansible.cfg file. No ansible.cfg file? Just create it:

[defaults]
ansible_managed = Please do not modify this file directly as it is managed by Ansible and could be overwritten.
retry_files_enabled = false
remote_user = core

[ssh_connection]
ssh_args = -F ssh.config

We’ll also disable retry_files, and specify that we want to use the “core” user when connecting to the machines using SSH.

I’ve already said this before, but CoreOS only ships with basic GNU utilities, which means no Python. And no Python means no Ansible, except for the raw module, the script module and the synchronize module. What we’re going to do is that we’re going to install a lightweight Python implementation called PyPy using only those modules, and then use that Python implementation in order to execute the rest of our playbook. Neat huh?

We’ll use the same role we used for the Kubernetes provisioning project. If you want to read more about it, like the technical explanation behind it, you can find all the information here.

So basically, we’ve got a role now under roles/bootstrap/ansible-bootstrap, which has 3 files under the tasks directory: main.yml, configure.yml and test.yml. The configure.yml file holds all the tasks necessary in order to install PyPy. The test.yml file verifies if Python is correctly installed by doing python --version. The main.yml file wraps these two files, adding the test tag to the test.yml part:

# filename: roles/bootstrap/ansible-bootstrap/tasks/main.yml
---

- name: Install and configure PyPy
  include: configure.yml

- name: Test PyPy installation
  include: test.yml
  tags: [ test ]

- name: Gather ansible facts
  setup:

I’ll follow this approach for each role on this project, so each role will have a smoketest, which will be enough to tell us if the component is correctly installed. This is pretty useful in order to test the already deployed infrastructure, as a conformance test, and check for deltas which might need to be corrected.

Now that we have our first role, it’s time to create a playbook. Since we’ll be deploying a Swarm cluster, I’ll just name it swam.yml:

---

- name: Bootstrap coreos hosts
  hosts: all
  gather_facts: false
  roles:
    - role: bootstrap/ansible-bootstrap
      tags: [ ansible-bootstrap ]

It’s quite straightforward so far, I’ll just launch the recently created role on each hosts, without gathering facts, since Python is not yet installed on the machines. Facts will be gathered at the end of the role though, as seen on the previous code snippet.

Next up, tests. We’ll use molecule for the win. I spoke to you all about molecule on a previous article. It is basically a testing tool for Ansible code. It creates ephemeral infrastructure (either virtual machines or containers), tests your roles on it (not only the execution of the roles, but also the syntax and their idempotence), and then it destroys it. Since there are no CoreOS containers, and Virtualbox virtual machines through Vagrant being the target platform, I’ll just use the Vagrant driver.

In order to test with molecule, I’m going to create a molecule.yml file, in which I’m going to define the Ansible files to use for the test, as well as the Vagrant machine’s specification and configuration.

First, I’ll specify which Ansible configuration to use, and which playbook to run:

---
ansible:
  config_file: ./ansible.cfg
  playbook: swarm.yml

Then, I’ll specify which Vagrant box to use for the virtual machines:

platforms:
- name: coreOS
  box: coreos-stable
  config.vm.box_url: https://storage.googleapis.com/stable.release.core-os.net/amd64-usr/current/coreos_production_vagrant.json

Then, what will my provider be, and how much physical resources will be used by each instance:

providers:
- name: virtualbox
  type: virtualbox
  options:
    memory: 2048
    cpus: 1

After that, I’ll define the specifics of each instance, including both hostname and IP addresses:

instances:
- name: swarm-manager-01
  ansible_groups:
    - swarm-leader
    - swarm-manager
  interfaces:
    - network_name: private_network
      type: static
      ip: 10.0.0.101
      auto_config: true
  options:
    append_platform_to_hostname: no
- name: swarm-manager-02
  ansible_groups:
    - swarm-manager
  interfaces:
    - network_name: private_network
      type: static
      ip: 10.0.0.102
      auto_config: true
  options:
    append_platform_to_hostname: no
- name: swarm-manager-03
  ansible_groups:
    - swarm-manager
  interfaces:
    - network_name: private_network
      type: static
      ip: 10.0.0.103
      auto_config: true
  options:
    append_platform_to_hostname: no
- name: swarm-worker-01
  ansible_groups:
    - swarm-worker
  interfaces:
    - network_name: private_network
      type: static
      ip: 10.0.0.121
      auto_config: true
  options:
    append_platform_to_hostname: no
...

With that in place, I just need to run molecule test in order to test that my infrastructure is created and configured correctly. This is actually an oversimplification of everything that can be done using molecule, but since I already wrote about it on a previous article, just can just head there and read about it if you’re really interested.

And with that, I also get to add two new (phony) Makefile target:

smoketest:
    @ansible-playbook -i inventories/vagrant.ini swarm.yml --tags test

test:
    @molecule test

The smoketest target allows me to run a conformance test on all the already deployed infrastructure, to check for deltas and see if something’s wrong whenever I want, and the test target allows me to test the code on fresh, newly created infrastructure, and to check for Ansible-specific good practices. Remember, this uses Molecule V1, so if you try to run it using Molecule V2 it will probably not work.

Tests are set up thusly. Moving on.

Manage: Lead and follow

Next up, we need to setup the three Manager nodes. I’ll start by creating a swarm-leader role, under the configuration roles directory:

roles/
├── bootstrap
│   └── ansible-bootstrap
└── configure
    └── swarm-leader
         └── tasks
             ├── configure.yml
             ├── main.yml
             └── test.yml

And for this role, we’ll use the same task division strategy we used before in order to add our smoketest.

First, the main.yml file, which is fairly simple, and only includes the other two yaml files, using a tag for the test file:

---

- name: Create Manager Leader
  include: configure.yml

- name: Test Manager Leader
  include: test.yml
  tags: [ test ]

The configure file first checks if the cluster is already on Swarm mode. If it is, it doesn’t do anything else. If it isn’t, it creates the first Swarm node, creating thus the Swarm cluster, which will be joined by the subsequent nodes. It also disables scheduling on the Leader, making sure that the Leader does not handle any workload and that it concentrates its resources on leading the cluster:

---

- name: Check if Swarm Mode is already activated
  command: docker info
  register: docker_info
  changed_when: false

- name: Create Swarm Manager Leader if it is not activated
  command: docker swarm init --advertise-addr 
  when: "'Swarm: active' not in docker_info.stdout"

- name: Disable Leader scheduling
  command: docker node update --availability drain 
  when: "'Swarm: active' not in docker_info.stdout and disable_leader_scheduling"

This last part is not actually necessary, specially for small clusters. Nevertheless it is usually a good practice, since the leader election process can be really intensive in terms of resource consumption. The disable_leader_scheduling variable is defined on the role’s defaults, and you can override it if you want your Leader to handle workloads.

Fairly simple. Notice the changed_when: false parameter on the first command task. It is there because running docker info will not change the state of the cluster, and it is therefore not a real action, just a way of collecting information.

Next, for the smoketest, I’ll verify if the created Manager node is in fact a Leader (which it should be, since it was the first Manager node to be created), and whether its status is “Drain”, since the Leader node is not supposed to handle any workload:

---

- name: Check if Manager node is Leader
  shell: docker node ls | grep 
  register: docker_info
  changed_when: false

- name: Fail if Manager node is not Leader
  assert:
    that:
      - "'Leader' in docker_info.stdout"
      - "'Active' in docker_info.stdout"

Now that the role is set, I’ll just add it to the swarm.yml playbook:

- name: Create Swarm Leader node
  hosts: swarm-leader
  roles:
    - role: configure/swarm-leader
      tags: [ swarm-leader ]

Using the host group we discussed earlier, and the proper tag in order to identify the action. And that’s it for the Manager Leader. We need some non-Manager Leader for that High Availability though!

So we’ll just repeat the previous process, we’ll create a swarm-manager role up next, with the same structure of the previous role (main.yml, configure.yml, test.yml).

I won’t show you the main.yml: it is basically the same one we saw before. The configure.yml file, on the other hand, checks if Swarm mode is activated on the node, the same way the Leader role does, but if it isn’t, it recovers the token needed to join the cluster as a Manager node from the Leader node, and joins the cluster with it. If Swarm mode is already activated, it does nothing:

---

- name: Check if Swarm Mode is already activated
  command: docker info
  register: docker_info
  changed_when: false

- name: Recover Swarm Leader token
  shell: docker swarm join-token manager | grep token | cut -d ' ' -f 6
  register: leader_token
  when: "'Swarm: active' not in docker_info.stdout"
  delegate_to: ""

- name: Join Swarm Cluster as Manager
  command: docker swarm join --token  
  when: "'Swarm: active' not in docker_info.stdout"

- name: Disable Manager scheduling
  command: docker node update --availability drain 
  when: "'Swarm: active' not in docker_info.stdout and disable_manager_scheduling"

Notice the delegate_to option on the token recovery task. This needs to be done because the token must be recovered from the Leader node and the Leader node only. Scheduling is also disabled on these nodes, by default, because of the reason specified above on the Leader node. This time, the disable_manager_scheduling variable is also defined on the role’s defaults. You can override this variable if you want your Managers to handle workloads.

The test file verifies different things as well:

---

- name: Check if node is Manager
  shell: docker node ls | grep 
  register: docker_info
  changed_when: false

- name: Fail if node is not Manager
  assert:
    that:
      - "'Reachable' in docker_info.stdout"
      - "'Drain' in docker_info.stdout"

It recovers the nodes information, and then it verifies that the node Manager type is Reachable rather than Leader, as it was for the Leader node. It also verifies that the nodes are “drained” since we don’t want them to run containers.

Finally, once the role is ready, I’ll add it to the swarm.yml file:

- name: Create Swarm Manager nodes
  hosts: swarm-manager:!swarm-leader
  roles:
    - role: configure/swarm-manager
      tags: [ swarm-manager ]

Notice the ! sign on the hosts part of the play. This specifies that we want to run the role on every node on the swarm-manager group, that isn’t in the swarm-leader group, thus preventing the Leader node to try to join the cluster as a non-Leader Manager. Sweet!

Once we finish all this, we should have everything we need Manager-wise. Time to get some Workers running! I’ll probably talk to you about that on the next article though.

Stay in touch!

Sebiwi

Sebiwi

Perfectionist

comments powered by Disqus
rss facebook twitter github youtube mail spotify instagram linkedin google google-plus pinterest medium vimeo stackoverflow reddit quora