Skip to main content
  1. Posts/

Here Comes the Swarm 03

·2 mins·

We’ve got our VMs running. Now it’s time to turn them into something useful. That’s where Ansible comes in—my trusty configuration management tool that does all the heavy lifting.

The Ansible Setup #

The playbooks live in ansible/. I use a few external roles (thank you, Galaxy) plus some custom roles I wrote myself:

  • geerlingguy.docker - installs Docker (because why reinvent the wheel)
  • geerlingguy.pip - Python dependencies
  • fresh_install - my custom role for base system configuration
  • docker_swarm - initializes the Swarm cluster
  • portainer - deploys the management UI
  • github_runner - sets up self-hosted GitHub Actions runners

Inventory: Knowing Your Nodes #

Here’s how Ansible sees my cluster:

[swarm_managers]
dkr-srv-1 ansible_host=10.0.30.21
dkr-srv-2 ansible_host=10.0.30.22
dkr-srv-3 ansible_host=10.0.30.23

[swarm_workers]
dkr-wrkr-1 ansible_host=10.0.30.31
dkr-wrkr-2 ansible_host=10.0.30.32
dkr-wrkr-3 ansible_host=10.0.30.33

[github_runners]
gh-runner-1 ansible_host=10.0.30.40

The Main Playbook #

The site.yml playbook is where the magic happens. It runs in two phases:

Phase 1: Configure All Nodes #

- name: Configure Swarm Manager Nodes
  hosts: all
  become: true
  
  roles:
    - geerlingguy.docker
    - geerlingguy.pip
    - fresh_install
    - docker_swarm

The fresh_install role does a lot of heavy lifting:

  • Locale setup
  • User management (adds my SSH key)
  • System updates
  • Package installation
  • LVM volume creation
  • Python setup
  • ZSH configuration
  • SSH banner (because why not)
  • CephFS mount
  • App data directories
  • NFS mounts (if needed)

Phase 2: Deploy Portainer #

- name: Post-Swarm Setup (Portainer)
  hosts: swarm_managers[0]  # Only on the leader
  roles:
    - portainer

Only runs on the first manager—Portainer handles the rest.

The Docker Swarm Role #

Here’s the fun part—initializing the swarm. Ansible does this intelligently:

  1. First manager (dkr-srv-1): Runs docker swarm init
  2. Other managers: Grab the join token and join as managers
  3. Workers: Grab the worker token and join as workers
- name: Initialize Docker Swarm on the first manager
  command: docker swarm init --advertise-addr {{ ansible_host }}
  when: inventory_hostname == groups['swarm_managers'][0]

- name: Join other managers to the swarm
  command: docker swarm join --token {{ swarm_manager_token }} ...
  when: inventory_hostname in groups['swarm_managers']

It also creates the shared Docker data directory on CephFS for persistent volumes.

Running It #

# Preview what will change
task ansible:site:plan

# Apply the configuration
task ansible:site:apply

Or run specific tags if you only need to update certain parts:

ansible-playbook site.yml --tags docker
ansible-playbook site.yml --tags swarm
ansible-playbook site.yml --tags portainer

The Result #

After Ansible finishes, I’ve got:

  • Docker installed on all nodes
  • All nodes joined to the swarm (3 managers, 3 workers)
  • Portainer running at portainer.krapulax.net
  • CephFS mounted at /mnt/cephfs
  • My user account with SSH access everywhere

Next up: deploying actual applications to the swarm!