--- # playbooks/docker/swarm_baseline.yml # Idempotent Ubuntu/Docker Swarm node baseline enforcement. # # ───────────────────────────────────────────────────────────────────────────── # PURPOSE: Ongoing drift enforcement — safe to run any time, safe to schedule. # Does NOT upgrade packages. Does NOT reboot. # For rolling OS updates: use playbooks/docker/swarm_update.yml # For cross-node consistency audit: use playbooks/docker/swarm_audit.yml # ───────────────────────────────────────────────────────────────────────────── # # What this enforces (all idempotent): # 0. Identity: Operational user, SSH key, passwordless sudo, docker group # 1. Packages: Required packages present (docker-ce, nfs-common, curl, htop) # 2. Storage: Swap disabled (swapoff -a + fstab commented) # 3. Sysctl: vm.swappiness=0, bridge netfilter, ip_forward (Docker requirements) # 4. Docker: /etc/docker/daemon.json with log rotation # # Usage: # # All swarm nodes: # ansible-playbook -i inventory/hosts.ini playbooks/docker/swarm_baseline.yml # # # Single node: # ansible-playbook -i inventory/hosts.ini playbooks/docker/swarm_baseline.yml --limit swarm-manager-1 # # # Dry-run: # ansible-playbook -i inventory/hosts.ini playbooks/docker/swarm_baseline.yml --check --diff # # # Target a specific section only: # ansible-playbook -i inventory/hosts.ini playbooks/docker/swarm_baseline.yml --tags sysctl # ansible-playbook -i inventory/hosts.ini playbooks/docker/swarm_baseline.yml --tags docker - name: Swarm node baseline enforcement hosts: swarm_hosts become: true vars: lab_user: "{{ lab_ansible_user | default('chester') }}" controller_ssh_pubkey_candidates: - "{{ lookup('env', 'HOME') }}/.ssh/id_ed25519_homelab.pub" - "{{ lookup('env', 'HOME') }}/.ssh/id_ed25519.pub" handlers: - name: Restart Docker ansible.builtin.service: name: docker state: restarted tasks: - name: "0. Identity: ensure user '{{ lab_user }}' is configured" tags: [identity, baseline] block: - name: "Ensure group '{{ lab_user }}' exists" ansible.builtin.group: name: "{{ lab_user }}" state: present - name: "Ensure user '{{ lab_user }}' exists with sudo and docker access" ansible.builtin.user: name: "{{ lab_user }}" group: "{{ lab_user }}" groups: - sudo - docker append: true shell: /bin/bash password: '!' password_lock: true - name: Locate SSH public key on control machine ansible.builtin.set_fact: controller_ssh_pubkey_path: >- {{ lookup('ansible.builtin.first_found', {'files': controller_ssh_pubkey_candidates, 'skip': true}) }} delegate_to: localhost become: false - name: Fail early if SSH public key is missing ansible.builtin.fail: msg: >- SSH public key not found on the control machine. Checked: {{ controller_ssh_pubkey_candidates | join(', ') }} Generate one with: ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 when: controller_ssh_pubkey_path | default('') | length == 0 - name: "Deploy SSH key to {{ lab_user }}" ansible.posix.authorized_key: user: "{{ lab_user }}" state: present key: "{{ lookup('file', controller_ssh_pubkey_path) }}" - name: "Grant '{{ lab_user }}' passwordless sudo" ansible.builtin.copy: dest: "/etc/sudoers.d/{{ lab_user }}" content: "{{ lab_user }} ALL=(ALL) NOPASSWD: ALL\n" mode: '0440' owner: root group: root validate: '/usr/sbin/visudo -cf %s' - name: "1. Packages: ensure required packages are present" tags: [packages, baseline] block: - name: Update apt cache ansible.builtin.apt: update_cache: true cache_valid_time: 3600 - name: Ensure required packages present ansible.builtin.apt: name: - docker-ce - docker-ce-cli - containerd.io - nfs-common - curl - htop - ca-certificates state: present - name: "2. Storage: disable swap" tags: [storage, baseline] block: - name: Disable swap immediately ansible.builtin.command: swapoff -a when: ansible_swaptotal_mb > 0 changed_when: ansible_swaptotal_mb > 0 - name: Comment out swap entries in /etc/fstab ansible.builtin.replace: path: /etc/fstab regexp: '^([^#].*\s+swap\s+.*)$' replace: '# \1' - name: "3. Sysctl: apply Docker Swarm networking parameters" tags: [sysctl, baseline] block: - name: Ensure br_netfilter module is loaded community.general.modprobe: name: br_netfilter state: present - name: Persist br_netfilter module load at boot ansible.builtin.copy: dest: /etc/modules-load.d/br_netfilter.conf content: "br_netfilter\n" owner: root group: root mode: '0644' - name: Apply and persist sysctl parameters for swarm ansible.posix.sysctl: name: "{{ item.key }}" value: "{{ item.value }}" sysctl_file: /etc/sysctl.d/90-swarm.conf state: present reload: true loop: - { key: vm.swappiness, value: "0" } - { key: net.bridge.bridge-nf-call-iptables, value: "1" } - { key: net.bridge.bridge-nf-call-ip6tables, value: "1" } - { key: net.ipv4.ip_forward, value: "1" } - name: "4. Docker: daemon configuration and log rotation" tags: [docker, baseline] block: - name: Ensure /etc/docker directory exists ansible.builtin.file: path: /etc/docker state: directory owner: root group: root mode: '0755' - name: Deploy Docker daemon.json with log rotation ansible.builtin.copy: dest: /etc/docker/daemon.json owner: root group: root mode: '0644' content: | { "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } } notify: Restart Docker