# Ansible — Provisioning Patterns > Canonical Ansible boilerplate for André Bassi's projects (reference: iacit MicroK8s cluster, wsl-linux-* setups). Phased playbooks, YAML inventories with hostgroups, roles for reusable hardening, secrets via pass, Taskfile as entrypoint. Point an LLM here and it writes playbooks exactly in this pattern. ## Hard Rules 1. Playbooks run only via Taskfile tasks (`ansible-playbook` wrapped in scripts/tasks, output with tee). 2. Inventory is YAML (`inventory/hosts.yaml`) with named hostgroups + `children` composition. Never INI. 3. Zero secrets hardcoded in playbooks. Sensitive values come from `pass` (looked up at deploy time) or are placeholders (`FILL_AFTER_KEYGEN`) filled by a task. 4. Idempotency first: prefer declarative modules (apt, sysctl, systemd, copy, blockinfile, cron, authorized_key) over shell. `shell`/`command` only when no module exists — then guard with `creates:`, `register` + `changed_when`, or `until`/`retries`. 5. Tags on every functional block (`tags: [base]`, `[wireguard]`, `[devtools]`) — selective runs are the norm. 6. `become: yes` at play level; drop to `become_user` for user-scope work (git clones, keygen, NVM). 7. Multi-phase playbooks: named phases in order, each phase a play. Orchestration from a single node uses `delegate_to`/`hosts: group[0]` + `include_tasks` loops. 8. Non-critical steps: `ignore_errors: yes` explicitly (never silently swallow critical failures). ## Repo Layout / ├── ansible/ │ ├── playbook-full.yaml # master orchestration (N phases) │ ├── playbook-setup.yaml # minimal variant │ ├── playbook-upgrade.yaml # in-place upgrades │ ├── inventory/ │ │ ├── hosts.yaml # real fleet │ │ └── localhost.yaml # local validation (kind, etc) │ ├── roles/ │ │ └── /tasks/main.yaml # prepare (OS hardening), microk8s, ... │ └── tasks/ │ └── .yaml # shared include_tasks (join-node.yaml) ├── scripts/ ├── Taskfile.yaml └── CLAUDE.md ## Inventory Pattern all: vars: ansible_user: iacit ansible_ssh_private_key_file: ~/.ssh/id_ed25519_iacit control_plane: hosts: ck8-controlplane1: { ansible_host: 10.1.1.171 } ck8-controlplane2: { ansible_host: 10.1.1.172 } ck8-controlplane3: { ansible_host: 10.1.1.173 } workers: hosts: ck8-worker1: { ansible_host: 10.1.1.174 } ck8-worker2: { ansible_host: 10.1.1.175 } microk8s: children: control_plane: workers: - Hostnames meaningful (`ck8-controlplane1`), groups by function, umbrella group via `children`. ## Phased Playbook Pattern - name: "Fase 1 — Preparar OS" hosts: microk8s become: yes roles: [prepare] - name: "Fase 2 — Instalar MicroK8s" hosts: microk8s become: yes roles: [microk8s] - name: "Fase 3 — Join nodes" hosts: control_plane[0] # orchestrate from first CP tasks: - command: microk8s kubectl cluster-info register: api_precheck until: api_precheck.rc == 0 retries: 18 delay: 10 - include_tasks: tasks/join-node.yaml loop: "{{ groups['control_plane'][1:] + groups['workers'] }}" - name: "Fase 3.5 — Labels e Taints" hosts: control_plane[0] tasks: - command: "{{ kubectl }} label node {{ item }} node-role.kubernetes.io/control-plane= --overwrite" loop: "{{ groups['control_plane'] }}" ## Role: OS Hardening (prepare) — canonical content - Base packages: snapd, open-iscsi, lvm2, conntrack, ipset, socat, chrony, apparmor. - sysctl tuning (30+ params) via `sysctl:` module with loop (ip_forward, bridge-nf-call-iptables, inotify limits, conntrack max...). - Kernel modules via `modprobe:`: br_netfilter, overlay, ip_vs*, nf_conntrack (+ persist in /etc/modules-load.d). - Swap off (`swapoff -a` + fstab edit), unattended-upgrades disabled. - UFW rules per service port list (K8s API 16443, kubelet 10250, Calico, MetalLB, Longhorn). ## Module Toolbox (preferred order) | Need | Module | |---|---| | packages | apt / apt_key / apt_repository | | kernel params | sysctl | | kernel modules | modprobe | | services | systemd | | file content blocks | blockinfile (managed markers em .zshrc, /etc/hosts) | | whole files | copy (content: \| inline) / template | | downloads | get_url + unarchive | | ssh | openssh_keypair, authorized_key | | cron | cron | | repos de user | git (com become_user) | | retry/health | register + until + retries + delay | ## Variables Pattern - name: WSL2 Ubuntu Setup hosts: wsl become: yes vars: wsl_user: TIN-000683 wsl_user_home: "/home/TIN-000683" mac_wg_ip: "10.10.0.2" aws_sso_url: "https://org.awsapps.com/start/#/" - Plain `vars:` at play level for environment facts; `group_vars/` when inventory grows. - Secrets: shell lookup at run time (`pass show wireguard/host/private-key`) or placeholder + fill task. Ansible Vault optional, pass is the default. ## File Deploy Pattern (config with secrets) - name: Deploy /etc/wireguard/wg0.conf copy: content: | [Interface] Address = {{ wg_ip }}/24 PrivateKey = {{ wg_private_key }} [Peer] PublicKey = {{ peer_pubkey }} Endpoint = {{ peer_endpoint }}:51820 AllowedIPs = 10.10.0.0/24 PersistentKeepalive = 25 dest: /etc/wireguard/wg0.conf mode: '0600' tags: [wireguard] ## Taskfile Integration setup: desc: Provisiona cluster completo cmds: - ansible-playbook -i ansible/inventory/hosts.yaml ansible/playbook-full.yaml 2>&1 | tee /tmp/ansible-full.log setup:tags: desc: 'Roda só uma tag. Uso: task setup:tags -- wireguard' cmds: - ansible-playbook -i ansible/inventory/hosts.yaml ansible/playbook-full.yaml --tags {{.CLI_ARGS}} 2>&1 | tee /tmp/ansible-tags.log ping: cmds: - ansible -i ansible/inventory/hosts.yaml all -m ping 2>&1 | tee /tmp/ansible-ping.log ## Validation Strategy - Local validation playbook against kind/localhost inventory before touching real fleet. - `--check` + `--diff` for dry-runs on existing hosts. - Health checks as tasks (register + until) instead of sleep.