├── .gitignore ├── LICENSE ├── README.md ├── Vagrantfile ├── library └── docker_info_facts ├── swarm-facts.yml └── swarm.yml /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.retry 3 | .vagrant 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 This End Out, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ansible Swarm Playbook 2 | 3 | Playbook for creating/managing a Docker Swarm cluster (requires Docker >= 1.12). 4 | 5 | Companion files to the following post: https://thisendout.com/2016/09/13/deploying-docker-swarm-with-ansible/ 6 | 7 | ## Assumptions 8 | 9 | This playbook assumes a running Docker daemon on the hosts and that the following Ansible inventory groups have been populated: `manager` and `worker`. 10 | 11 | ## Variables 12 | 13 | You can override the `swarm_iface` variable in Ansible to determine the listening interface for your swarm hosts. 14 | 15 | ## `swarm.yml` vs. `swarm-facts.yml` 16 | 17 | The `swarm.yml` playbook uses a shell statement for determining cluster membership where the `swarm-facts.yml` playbook uses the `docker_info_facts` module for injecting Docker info as facts then checking cluster membership against that. 18 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure(2) do |config| 5 | #config.vm.box = "ubuntu/xenial64" 6 | config.vm.box = "boonen/docker_debian-jessie64" 7 | # config.vm.box_check_update = false 8 | config.vm.synced_folder ".", "/vagrant", disabled: true 9 | 10 | config.vm.provider "virtualbox" do |v| 11 | v.memory = 256 12 | end 13 | 14 | config.vm.provision "shell", inline: <<-SHELL 15 | apt-get update 16 | apt-get install -y python python-pip 17 | pip install docker-py 18 | SHELL 19 | 20 | (1..6).each do |i| 21 | if [1,2,3].include?(i) 22 | config.vm.define "manager-#{i}" do |node| 23 | node.vm.hostname = "manager-#{i}" 24 | node.vm.network "private_network", ip: "192.168.33.1#{i}" 25 | # node.vm.network "forwarded_port", guest: 80, host: 8080 26 | end 27 | else 28 | config.vm.define "worker-#{i}" do |node| 29 | node.vm.hostname = "worker-#{i}" 30 | node.vm.network "private_network", ip: "192.168.33.1#{i}" 31 | # node.vm.network "forwarded_port", guest: 80, host: 8080 32 | # hack to only run once at the end 33 | if i == 6 34 | node.vm.provision "ansible" do |ansible| 35 | ansible.playbook = "swarm.yml" 36 | #ansible.playbook = "swarm-facts.yml" 37 | ansible.limit = "all" 38 | ansible.extra_vars = { 39 | swarm_iface: "eth1" 40 | } 41 | ansible.groups = { 42 | "manager" => ["manager-[1:3]"], 43 | "worker" => ["worker-[4:6]"], 44 | } 45 | ansible.raw_arguments = [ 46 | "-M ./library" 47 | ] 48 | end 49 | end 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /library/docker_info_facts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2016, This End Out, LLC. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | DOCUMENTATION = """ 17 | --- 18 | module: docker_info_facts 19 | short_description: 20 | - A module for injecting Docker info as facts. 21 | description: 22 | - A module for injecting Docker info as facts. 23 | author: nextrevision 24 | """ 25 | 26 | EXAMPLES = """ 27 | - name: load docker info facts 28 | docker_info_facts: 29 | """ 30 | 31 | docker_lib_missing=False 32 | 33 | try: 34 | from docker import Client 35 | except: 36 | try: 37 | from docker import APIClient as Client 38 | except: 39 | docker_lib_missing=True 40 | 41 | 42 | def _get_docker_info(): 43 | try: 44 | return Client().info(), False 45 | except Exception as e: 46 | return {}, e.message 47 | 48 | 49 | def main(): 50 | module = AnsibleModule( 51 | argument_spec=dict(), 52 | supports_check_mode=False 53 | ) 54 | 55 | if docker_lib_missing: 56 | msg = "Could not load docker python library; please install docker-py or docker library" 57 | module.fail_json(msg=msg) 58 | 59 | info, err = _get_docker_info() 60 | 61 | if err: 62 | module.fail_json(msg=err) 63 | 64 | module.exit_json( 65 | changed=True, 66 | ansible_facts={'docker_info': info}) 67 | 68 | 69 | from ansible.module_utils.basic import * 70 | if __name__ == '__main__': 71 | main() 72 | -------------------------------------------------------------------------------- /swarm-facts.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # determine the status of each manager node and break them 3 | # into two groups: 4 | # - swarm_manager_operational (swarm is running and active) 5 | # - swarm_manager_bootstrap (host needs to be joined to the cluster) 6 | - hosts: manager 7 | become: true 8 | tasks: 9 | - name: load docker info as facts 10 | docker_info_facts: 11 | 12 | - name: create swarm_manager_operational group 13 | add_host: 14 | hostname: "{{ item }}" 15 | groups: swarm_manager_operational 16 | with_items: "{{ ansible_play_hosts | default(play_hosts) }}" 17 | when: "'{{ hostvars[item]['docker_info']['Swarm']['LocalNodeState'] }}' == 'active'" 18 | run_once: true 19 | 20 | - name: create swarm_manager_bootstrap group 21 | add_host: 22 | hostname: "{{ item }}" 23 | groups: swarm_manager_bootstrap 24 | with_items: "{{ ansible_play_hosts | default(play_hosts) }}" 25 | when: "'{{ hostvars[item]['docker_info']['Swarm']['LocalNodeState'] }}' != 'active'" 26 | run_once: true 27 | 28 | # determine the status of each worker node and break them 29 | # into two groups: 30 | # - swarm_worker_operational (host is joined to the swarm cluster) 31 | # - swarm_worker_bootstrap (host needs to be joined to the cluster) 32 | - hosts: worker 33 | become: true 34 | tasks: 35 | - name: load docker info as facts 36 | docker_info_facts: 37 | 38 | - name: create swarm_worker_operational group 39 | add_host: 40 | hostname: "{{ item }}" 41 | groups: swarm_worker_operational 42 | with_items: "{{ ansible_play_hosts | default(play_hosts) }}" 43 | when: "'{{ hostvars[item]['docker_info']['Swarm']['LocalNodeState'] }}' == 'active'" 44 | run_once: true 45 | 46 | - name: create swarm_worker_bootstrap group 47 | add_host: 48 | hostname: "{{ item }}" 49 | groups: swarm_worker_bootstrap 50 | with_items: "{{ ansible_play_hosts | default(play_hosts) }}" 51 | when: "'{{ hostvars[item]['docker_info']['Swarm']['LocalNodeState'] }}' != 'active'" 52 | run_once: true 53 | 54 | # when the swarm_manager_operational group is empty, meaning there 55 | # are no hosts running swarm, we need to initialize one of the hosts 56 | # then add it to the swarm_manager_operational group 57 | - hosts: swarm_manager_bootstrap[0] 58 | become: true 59 | tasks: 60 | - name: initialize swarm cluster 61 | shell: > 62 | docker swarm init 63 | --advertise-addr={{ swarm_iface | default('eth0') }}:2377 64 | when: "'swarm_manager_operational' not in groups" 65 | register: bootstrap_first_node 66 | 67 | - name: add initialized host to swarm_manager_operational group 68 | add_host: 69 | hostname: "{{ item }}" 70 | groups: swarm_manager_operational 71 | with_items: "{{ ansible_play_hosts | default(play_hosts) }}" 72 | when: bootstrap_first_node.changed 73 | 74 | # retrieve the swarm tokens and populate a list of ips listening on 75 | # the swarm port 2377 76 | - hosts: swarm_manager_operational[0] 77 | become: true 78 | vars: 79 | iface: "{{ swarm_iface | default('eth0') }}" 80 | tasks: 81 | - name: retrieve swarm manager token 82 | shell: docker swarm join-token -q manager 83 | register: swarm_manager_token 84 | 85 | - name: retrieve swarm worker token 86 | shell: docker swarm join-token -q worker 87 | register: swarm_worker_token 88 | 89 | - name: populate list of manager ips 90 | add_host: 91 | hostname: "{{ hostvars[item]['ansible_' + iface]['ipv4']['address'] }}" 92 | groups: swarm_manager_ips 93 | with_items: "{{ ansible_play_hosts | default(play_hosts) }}" 94 | 95 | # join the hosts not yet initialized to the swarm cluster 96 | - hosts: swarm_manager_bootstrap:!swarm_manager_operational 97 | become: true 98 | vars: 99 | token: "{{ hostvars[groups['swarm_manager_operational'][0]]['swarm_manager_token']['stdout'] }}" 100 | tasks: 101 | - name: join manager nodes to cluster 102 | shell: > 103 | docker swarm join 104 | --advertise-addr={{ swarm_iface | default('eth0') }}:2377 105 | --token={{ token }} 106 | {{ groups['swarm_manager_ips'][0] }}:2377 107 | 108 | # join the remaining workers to the swarm cluster 109 | - hosts: swarm_worker_bootstrap 110 | become: true 111 | vars: 112 | token: "{{ hostvars[groups['swarm_manager_operational'][0]]['swarm_worker_token']['stdout'] }}" 113 | tasks: 114 | - name: join worker nodes to cluster 115 | shell: > 116 | docker swarm join 117 | --advertise-addr={{ swarm_iface | default('eth0') }}:2377 118 | --token={{ token }} 119 | {{ groups['swarm_manager_ips'][0] }}:2377 120 | -------------------------------------------------------------------------------- /swarm.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # determine the status of each manager node and break them 3 | # into two groups: 4 | # - swarm_manager_operational (swarm is running and active) 5 | # - swarm_manager_bootstrap (host needs to be joined to the cluster) 6 | - hosts: manager 7 | become: true 8 | tasks: 9 | - name: determine swarm status 10 | shell: > 11 | docker info --format \{\{.Swarm.LocalNodeState\}\} 12 | register: swarm_status 13 | 14 | - name: create swarm_manager_operational group 15 | add_host: 16 | hostname: "{{ item }}" 17 | groups: swarm_manager_operational 18 | with_items: "{{ ansible_play_hosts | default(play_hosts) }}" 19 | when: "'active' in hostvars[item].swarm_status.stdout_lines" 20 | run_once: true 21 | 22 | - name: create swarm_manager_bootstrap group 23 | add_host: 24 | hostname: "{{ item }}" 25 | groups: swarm_manager_bootstrap 26 | with_items: "{{ ansible_play_hosts | default(play_hosts) }}" 27 | when: "'active' not in hostvars[item].swarm_status.stdout_lines" 28 | run_once: true 29 | 30 | # determine the status of each worker node and break them 31 | # into two groups: 32 | # - swarm_worker_operational (host is joined to the swarm cluster) 33 | # - swarm_worker_bootstrap (host needs to be joined to the cluster) 34 | - hosts: worker 35 | become: true 36 | tasks: 37 | - name: determine swarm status 38 | shell: > 39 | docker info --format \{\{.Swarm.LocalNodeState\}\} 40 | register: swarm_status 41 | 42 | - name: create swarm_worker_operational group 43 | add_host: 44 | hostname: "{{ item }}" 45 | groups: swarm_worker_operational 46 | with_items: "{{ ansible_play_hosts | default(play_hosts) }}" 47 | when: "'active' in hostvars[item].swarm_status.stdout_lines" 48 | run_once: true 49 | 50 | - name: create swarm_worker_bootstrap group 51 | add_host: 52 | hostname: "{{ item }}" 53 | groups: swarm_worker_bootstrap 54 | with_items: "{{ ansible_play_hosts | default(play_hosts) }}" 55 | when: "'active' not in hostvars[item].swarm_status.stdout_lines" 56 | run_once: true 57 | 58 | # when the swarm_manager_operational group is empty, meaning there 59 | # are no hosts running swarm, we need to initialize one of the hosts 60 | # then add it to the swarm_manager_operational group 61 | - hosts: swarm_manager_bootstrap[0] 62 | become: true 63 | tasks: 64 | - name: initialize swarm cluster 65 | shell: > 66 | docker swarm init 67 | --advertise-addr={{ swarm_iface | default('eth0') }}:2377 68 | when: "'swarm_manager_operational' not in groups" 69 | register: bootstrap_first_node 70 | 71 | - name: add initialized host to swarm_manager_operational group 72 | add_host: 73 | hostname: "{{ item }}" 74 | groups: swarm_manager_operational 75 | with_items: "{{ ansible_play_hosts | default(play_hosts) }}" 76 | when: bootstrap_first_node.changed 77 | 78 | # retrieve the swarm tokens and populate a list of ips listening on 79 | # the swarm port 2377 80 | - hosts: swarm_manager_operational[0] 81 | become: true 82 | vars: 83 | iface: "{{ swarm_iface | default('eth0') }}" 84 | tasks: 85 | - name: retrieve swarm manager token 86 | shell: docker swarm join-token -q manager 87 | register: swarm_manager_token 88 | 89 | - name: retrieve swarm worker token 90 | shell: docker swarm join-token -q worker 91 | register: swarm_worker_token 92 | 93 | - name: populate list of manager ips 94 | add_host: 95 | hostname: "{{ hostvars[item]['ansible_' + iface]['ipv4']['address'] }}" 96 | groups: swarm_manager_ips 97 | with_items: "{{ ansible_play_hosts | default(play_hosts) }}" 98 | 99 | # join the manager hosts not yet initialized to the swarm cluster 100 | - hosts: swarm_manager_bootstrap:!swarm_manager_operational 101 | become: true 102 | vars: 103 | token: "{{ hostvars[groups['swarm_manager_operational'][0]]['swarm_manager_token']['stdout'] }}" 104 | tasks: 105 | - name: join manager nodes to cluster 106 | shell: > 107 | docker swarm join 108 | --advertise-addr={{ swarm_iface | default('eth0') }}:2377 109 | --token={{ token }} 110 | {{ groups['swarm_manager_ips'][0] }}:2377 111 | 112 | # join the worker hosts not yet initialized to the swarm cluster 113 | - hosts: swarm_worker_bootstrap 114 | become: true 115 | vars: 116 | token: "{{ hostvars[groups['swarm_manager_operational'][0]]['swarm_worker_token']['stdout'] }}" 117 | tasks: 118 | - name: join worker nodes to cluster 119 | shell: > 120 | docker swarm join 121 | --advertise-addr={{ swarm_iface | default('eth0') }}:2377 122 | --token={{ token }} 123 | {{ groups['swarm_manager_ips'][0] }}:2377 124 | --------------------------------------------------------------------------------