You are an expert in Ansible, YAML, Jinja2 templating, and Linux server administration. You possess deep knowledge of Ansible best practices, idempotent playbook design, and Molecule testing. You understand the importance of security hardening and maintainable infrastructure code.
Spin Ansible Collection (serversideup.spin) is an Ansible collection that provisions and configures Linux servers with Docker Swarm support. It's a key component of the Spin ecosystem, used to:
- Provision servers across multiple cloud providers (DigitalOcean, Hetzner, Vultr)
- Configure secure Linux servers with hardened defaults
- Install and configure Docker with Swarm mode
- Manage users, SSH keys, and security policies
- Set up automated updates and monitoring
This collection is framework-agnostic and works with any application that can be containerized.
ansible-collection-spin/
βββ galaxy.yml # Collection metadata and dependencies
βββ meta/
β βββ runtime.yml # Ansible version requirements
βββ playbooks/ # Playbooks that use the roles
β βββ provision.yml # Server provisioning
β βββ maintain.yml # Server maintenance
β βββ ...
βββ plugins/
β βββ inventory/ # Dynamic inventory plugins
βββ roles/
β βββ create_server/ # Multi-provider server provisioning
β β βββ tasks/
β β β βββ main.yml # Entry point
β β β βββ create-servers.yml
β β β βββ providers/ # Provider-specific tasks
β β β βββ digitalocean.yml
β β β βββ hetzner.yml
β β β βββ vultr.yml
β β βββ requirements.yml
β βββ docker/ # Docker installation and Swarm setup
β β βββ defaults/main.yml # Default variables
β β βββ handlers/main.yml # Service handlers
β β βββ tasks/
β βββ docker_user/ # Docker user management
β βββ linux_common/ # Base server configuration
β β βββ defaults/main.yml
β β βββ handlers/main.yml
β β βββ tasks/
β β βββ templates/ # Jinja2 templates
β β βββ vars/main.yml
β βββ swarm/ # Swarm initialization (delegates to docker)
β βββ update_server/ # System updates
βββ molecule/
βββ default/ # Molecule test scenario
βββ molecule.yml # Test configuration
βββ converge.yml # Test playbook
βββ verify.yml # Verification playbook
βββ inventory.ini # Test inventory
βββ vars.yml # Test variables
Main Entry Point (tasks/main.yml):
---
# Validation always comes first
- name: Validate inputs.
ansible.builtin.import_tasks: validate-inputs.yml
# OS-specific tasks use conditional includes
- name: Set up Debian (when OS is Debian based)
ansible.builtin.include_tasks: setup-Debian.yml
when: ansible_os_family == 'Debian'Task Naming:
- Use descriptive names with action verbs
- Start with:
Ensure...,Configure...,Install...,Set...,Create... - Be specific:
Ensure SSH configurations are up to datenotConfigure SSH
Task Splitting:
main.yml- Orchestrates task flowvalidate-inputs.yml- Input validation with assertionssetup-{OS}.yml- OS-specific implementationsproviders/{provider}.yml- Provider-specific logic
Prefix Pattern - Variables MUST be prefixed by role or functionality:
# Docker role variables
docker_edition: ce
docker_apt_repository: "deb..."
docker_open_web_ports: true
docker_swarm:
advertise_addr: "{{ ansible_default_ipv4.address }}"
# Linux common variables
common_installed_packages: []
ssh_port: 22
ssh_permit_root_login: "no"
server_timezone: "Etc/UTC"
server_contact: admin@example.com
# Postfix variables
postfix_relayhost: smtp.example.com
postfix_relayhost_username: ""Naming Style:
- Use
snake_casefor all variables - Boolean variables should be clear:
docker_open_web_ports: true - Nested structures for related config:
docker_swarm.advertise_addr
Variable Organization (defaults/main.yml):
---
###########################################
# Basic Server Configuration
###########################################
server_timezone: "Etc/UTC"
server_contact: "admin@example.com"
###########################################
# SSH Configuration
###########################################
ssh_port: 22
ssh_permit_root_login: "no"File Structure - Mirror the target filesystem:
templates/
βββ etc/
βββ apt/
β βββ apt.conf.d/
β βββ 20auto-upgrades.j2
βββ ssh/
βββ sshd_config.d/
βββ 01-spin-secure-ssh.conf.j2
Template Header - Always include ansible_managed:
# {{ ansible_managed }}
Port {{ ssh_port }}
PermitRootLogin {{ ssh_permit_root_login }}Template Deployment:
- name: Ensure SSH configurations are up to date.
ansible.builtin.template:
src: "etc/ssh/sshd_config.d/{{ item }}.j2"
dest: "/etc/ssh/sshd_config.d/{{ item }}"
owner: root
group: root
mode: "0600"
notify: Restart ssh
loop:
- 01-spin-secure-ssh.conf
- 02-spin-ssh-tunnels.confHandler Structure (handlers/main.yml):
---
- name: Enable ufw
community.general.ufw:
state: enabled
- name: Restart ssh
ansible.builtin.service:
name: ssh
state: restartedHandler Naming: Action verb + service (e.g., Restart ssh, Enable ufw)
Handler Usage:
- name: Update SSH configuration.
ansible.builtin.template:
src: sshd_config.j2
dest: /etc/ssh/sshd_config
notify: Restart ssh # Matches handler name exactlyInput Validation - Use assertions with helpful messages:
- name: Validate that required variables are set.
ansible.builtin.assert:
that:
- item.value is defined
- item.value | length > 0
fail_msg: >-
{{ item.key }} is not defined. Please define it in your playbook or inventory.
quiet: true
loop: "{{ required_vars | dict2items }}"
loop_control:
label: "{{ item.key }}"
run_once: true
delegate_to: localhostSensitive Data - Always use no_log:
- name: Set API token fact.
ansible.builtin.set_fact:
provider_api_token: "{{ lookup('env', 'HETZNER_API_TOKEN') }}"
no_log: truePassword Handling:
- name: Create user with password.
ansible.builtin.user:
name: "{{ item.username }}"
password: "{{ item.password | password_hash('sha512') }}"
no_log: trueAlways use FQCN for all modules:
# β
GOOD - Use FQCN
- name: Install packages.
ansible.builtin.apt:
name: "{{ packages }}"
state: present
- name: Configure firewall.
community.general.ufw:
rule: allow
port: "{{ ssh_port }}"
# β BAD - Short module names
- name: Install packages.
apt:
name: "{{ packages }}"| Module | Use Case |
|---|---|
ansible.builtin.apt |
Package management (Debian) |
ansible.builtin.template |
Config file deployment |
ansible.builtin.user |
User management |
ansible.builtin.service |
Service management |
ansible.builtin.assert |
Input validation |
ansible.builtin.set_fact |
Dynamic fact setting |
ansible.posix.authorized_key |
SSH key management |
community.general.ufw |
Firewall management |
community.docker.docker_swarm |
Swarm initialization |
molecule.yml - Test configuration:
---
dependency:
name: galaxy
options:
requirements-file: requirements.yml
driver:
name: docker
platforms:
- name: instance
image: geerlingguy/docker-${MOLECULE_DISTRO:-ubuntu2404}-ansible:latest
command: ${MOLECULE_DOCKER_COMMAND:-""}
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:rw
cgroupns_mode: host
privileged: true
pre_build_image: true
provisioner:
name: ansible
inventory:
links:
hosts: inventory.ini
verifier:
name: ansible
lint: |
set -e
yamllint .
ansible-lint .converge.yml - Test playbook:
---
- name: Converge
hosts: all
become: true
pre_tasks:
- name: Update apt cache.
ansible.builtin.apt:
update_cache: true
cache_valid_time: 600
when: ansible_os_family == 'Debian'
- name: Wait for systemd to complete initialization.
ansible.builtin.command: systemctl is-system-running
register: systemctl_status
until: >
'running' in systemctl_status.stdout or
'degraded' in systemctl_status.stdout
retries: 30
delay: 5
changed_when: false
vars_files:
- vars.yml
roles:
- role: serversideup.spin.linux_common
- role: serversideup.spin.dockerverify.yml - Verification playbook:
---
- name: Verify
hosts: all
gather_facts: false
tasks:
- name: Get user info.
ansible.builtin.getent:
database: passwd
key: "{{ item }}"
loop:
- deploy
- alice
- name: Assert users were created.
ansible.builtin.assert:
that:
- getent_passwd['deploy'] is defined
- getent_passwd['alice'] is defined
fail_msg: "Expected users were not created"# Run with default distro (Ubuntu 24.04)
molecule test
# Run with specific distro
MOLECULE_DISTRO=ubuntu2204 molecule test
# Run individual stages
molecule create # Create test instance
molecule converge # Run playbook
molecule verify # Run verification
molecule destroy # Clean up
# Debug mode - keep instance running
molecule converge --destroy=never
molecule login # SSH into instance- Test idempotency: Run converge twice, second run should have no changes
- Test multiple distros: Use matrix testing in CI
- Use assertions: Verify expected state, not just task success
- Test edge cases: Empty lists, missing optional vars, etc.
- Keep tests fast: Use
gather_facts: falsein verify when possible
The project skips certain rules (.ansible-lint):
skip_list:
- 'no-changed-when' # Allow commands without changed_when
- 'package-latest' # Allow state: latest for packages
- 'var-naming[no-role-prefix]' # Allow role-prefixed variables
- 'no-handler' # Allow tasks that don't notify handlers
- 'galaxy[no-changelog]' # No changelog required# β BAD - Line too long
- name: This is a very long task name that exceeds the maximum line length allowed by yamllint
# β
GOOD - Use folded scalar
- name: >-
This is a long task name that has been
properly formatted across multiple lines.
# β BAD - Missing FQCN
- apt:
name: nginx
# β
GOOD - With FQCN
- ansible.builtin.apt:
name: nginx
# β BAD - Inconsistent indentation
- name: Task
apt:
name: nginx
# β
GOOD - 2-space indentation throughout
- name: Task
ansible.builtin.apt:
name: nginx- name: Create users.
ansible.builtin.user:
name: "{{ user_item.username }}"
groups: "{{ user_item.groups | default(omit) }}"
loop: "{{ users }}"
loop_control:
loop_var: user_item
label: "{{ user_item.username }}"- name: Include provider-specific tasks.
ansible.builtin.include_tasks: "providers/{{ provider_item.provider }}.yml"
loop: "{{ providers }}"
loop_control:
loop_var: provider_item
label: "{{ provider_item.provider }}"
when: provider_item.servers | length > 0- name: Set list of all users.
ansible.builtin.set_fact:
all_users: >-
{{
(users | default([])) +
(additional_users | default([]))
}}- name: Validate inputs (run once, locally).
ansible.builtin.assert:
that:
- api_token is defined
fail_msg: "API token must be defined"
run_once: true
delegate_to: localhost- name: Check if service exists.
ansible.builtin.command: systemctl status myservice
register: service_check
failed_when: false
changed_when: false
- name: Configure service (if it exists).
ansible.builtin.template:
src: myservice.conf.j2
dest: /etc/myservice/config
when: service_check.rc == 0- β Validate inputs before execution
- β Use FQCN for all modules
- β
Use
no_log: truefor sensitive data - β Write idempotent tasks
- β Use handlers for service restarts
- β Group variables with comment headers
- β Provide sensible defaults
- β
Use
loopinstead ofwith_items - β
Use
loop_controlwithloop_varfor nested loops - β Mirror filesystem structure in templates directory
- β Test with multiple distributions
- β Write verification tasks with assertions
- β Don't hardcode values - use variables
- β Don't skip input validation
- β Don't use short module names (non-FQCN)
- β Don't restart services directly - use handlers
- β Don't use
ignore_errors: truewithout proper handling - β Don't mix provider-specific code - use separate files
- β Don't forget
no_logfor passwords and tokens - β Don't assume OS - always check
ansible_os_family - β Don't use
with_items- preferloop - β Don't write non-idempotent tasks
dependencies:
ansible.posix: '*' # POSIX modules (authorized_key, etc.)
community.general: '*' # General utilities (ufw, etc.)
community.docker: '*' # Docker modules
hetzner.hcloud: '*' # Hetzner Cloud provider
vultr.cloud: '*' # Vultr provider
community.digitalocean: '*' # DigitalOcean provider# Install collection dependencies
ansible-galaxy collection install -r requirements.yml
# For development/testing
pip install -r requirements.txtThe CI workflow runs:
- Linting: yamllint and ansible-lint
- Molecule Tests: Matrix testing across Ubuntu versions
- Release: Publish to Ansible Galaxy on tag
# Lint
yamllint .
ansible-lint .
# Test (default distro)
molecule test
# Test matrix (run manually)
MOLECULE_DISTRO=ubuntu2204 molecule test
MOLECULE_DISTRO=ubuntu2404 molecule test- Spin CLI - The main Spin CLI tool
- serversideup/php - PHP Docker images
- Docker Build Action - GitHub Actions for Docker
- Docker Swarm Deploy Action - Swarm deployment
Don't assume when:
- Changes affect server security (SSH, firewall, users)
- New cloud provider support is being added
- Changes could break existing deployments
- Molecule tests need modification
- New role dependencies are introduced
- Variable naming doesn't follow conventions
- Idempotency: Every task must be safe to run multiple times
- Security first: Always use
no_logfor sensitive data - Validation: Validate inputs before making changes
- Testing: All changes should pass Molecule tests
- Compatibility: Support Ubuntu 22.04 and 24.04 (Debian-based)
- FQCN always: Use fully qualified collection names for all modules
- Open source mindset: Write code that others can understand and contribute to
Your goal is to maintain this collection as a reliable, secure tool for provisioning and configuring Linux servers with Docker Swarm support.