This skill guides writing Ansible playbooks for server configuration. Use when hardening servers, installing packages, or automating post-provisioning tasks that cloud-init cannot handle.
Writes Ansible playbooks for server configuration. Use when hardening servers, installing packages, or automating post-provisioning tasks that cloud-init cannot handle.
/plugin marketplace add majesticlabs-dev/majestic-marketplace/plugin install majestic-devops@majestic-marketplaceThis skill is limited to using the following tools:
ALWAYS start with the simplest approach. Only add complexity when explicitly requested.
| Aspect | ✅ Simple (Default) | ❌ Overengineered |
|---|---|---|
| Playbooks | 1 playbook with inline tasks | Multiple playbooks + custom roles |
| Roles | Use Galaxy roles (geerlingguy.*) | Write custom roles for simple tasks |
| Inventory | Single hosts.ini | Multiple inventories + group_vars hierarchy |
| Variables | Inline in playbook or single vars file | Scattered across group_vars/host_vars |
| File count | ~3-5 files total | 20+ files in nested directories |
Rule: If you can fit everything in one 200-line playbook, DO IT.
| Use Cloud-Init When | Use Ansible When |
|---|---|
| First boot only | Re-running config on existing servers |
| Simple package install | Complex multi-step configuration |
| Basic user creation | Role-based configuration |
| Immutable infrastructure | Mutable servers needing updates |
Rule of thumb: Cloud-init for initial provisioning, Ansible for ongoing management.
infra/ansible/
├── playbook.yml # Single playbook with all tasks inline
├── requirements.yml # Galaxy dependencies (geerlingguy.*, etc.)
├── hosts.ini # Inventory (git-ignored)
└── hosts.ini.example # Inventory template
infra/ansible/
├── playbook.yml # Main playbook
├── requirements.yml # Galaxy dependencies
├── hosts.ini # Inventory (git-ignored)
├── hosts.ini.example # Inventory template
├── group_vars/
│ └── all.yml # Shared variables
└── roles/
└── custom_role/
├── tasks/main.yml
├── handlers/main.yml
└── templates/
# hosts.ini
[web]
192.168.1.1 ansible_user=root
[db]
192.168.1.2 ansible_user=root
[all:vars]
ansible_python_interpreter=/usr/bin/python3
# Generate inventory from Terraform output
SERVER_IP=$(cd infra && tofu output -raw server_ip)
cat > infra/ansible/hosts.ini << EOF
[web]
$SERVER_IP ansible_user=root
EOF
---
- name: Configure web servers
hosts: web
become: true
vars:
timezone: "UTC"
swap_size_mb: "2048"
tasks:
- name: Update apt cache
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
- name: Install packages
ansible.builtin.apt:
name:
- docker.io
- fail2ban
- ufw
state: present
---
- name: Configure web servers
hosts: web
become: true
vars:
security_autoupdate_reboot: true
security_autoupdate_reboot_time: "03:00"
roles:
- role: geerlingguy.swap
when: ansible_swaptotal_mb < 1
- role: geerlingguy.docker
- role: security
- name: Install required packages
ansible.builtin.apt:
name:
- curl
- ca-certificates
- gnupg
- fail2ban
- ufw
- ntp
state: present
update_cache: true
- name: Check if Docker is installed
ansible.builtin.command: docker --version
register: docker_installed
ignore_errors: true
changed_when: false
- name: Install Docker via convenience script
ansible.builtin.shell: curl -fsSL https://get.docker.com | sh
when: docker_installed.rc != 0
args:
creates: /usr/bin/docker
- name: Ensure Docker is running
ansible.builtin.systemd:
name: docker
state: started
enabled: true
- name: Disable SSH password authentication
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: "^#?PasswordAuthentication"
line: "PasswordAuthentication no"
notify: Restart ssh
- name: Disable SSH root login with password
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: "^#?PermitRootLogin"
line: "PermitRootLogin prohibit-password"
notify: Restart ssh
handlers:
- name: Restart ssh
ansible.builtin.systemd:
name: ssh # Ubuntu uses 'ssh', not 'sshd'
state: restarted
- name: Configure fail2ban for SSH
ansible.builtin.copy:
dest: /etc/fail2ban/jail.local
content: |
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 5
bantime = 3600
findtime = 600
mode: "0644"
notify: Restart fail2ban
- name: Ensure fail2ban is running
ansible.builtin.systemd:
name: fail2ban
state: started
enabled: true
handlers:
- name: Restart fail2ban
ansible.builtin.systemd:
name: fail2ban
state: restarted
- name: Set UFW default policies
community.general.ufw:
direction: "{{ item.direction }}"
policy: "{{ item.policy }}"
loop:
- { direction: incoming, policy: deny }
- { direction: outgoing, policy: allow }
- name: Allow specified ports through UFW
community.general.ufw:
rule: allow
port: "{{ item }}"
proto: tcp
loop:
- 22 # SSH
- 80 # HTTP
- 443 # HTTPS
- name: Enable UFW
community.general.ufw:
state: enabled
- name: Configure sysctl for performance
ansible.posix.sysctl:
name: "{{ item.name }}"
value: "{{ item.value }}"
state: present
reload: true
loop:
- { name: vm.swappiness, value: "10" }
- { name: net.core.somaxconn, value: "65535" }
- name: Set timezone
community.general.timezone:
name: "{{ timezone }}"
- name: Remove snapd
ansible.builtin.apt:
name: snapd
state: absent
purge: true
ignore_errors: true
- name: Remove snap directories
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop:
- /snap
- /var/snap
- /var/lib/snapd
---
roles:
- name: geerlingguy.swap
version: 2.0.0
- name: geerlingguy.docker
version: 7.4.1
collections:
- name: community.general
- name: ansible.posix
ansible-galaxy install -r requirements.yml --force
ANSIBLE_HOST_KEY_CHECKING=False ansible-playbook -i hosts.ini playbook.yml
ansible-playbook -i hosts.ini playbook.yml \
-e "timezone=Europe/Berlin" \
-e "swap_size_mb=4096"
ansible-playbook -i hosts.ini playbook.yml --check --diff
ansible-playbook -i hosts.ini playbook.yml --limit web
Complete playbook for Kamal deployment servers (based on kamal-ansible-manager):
---
- name: Prepare server for Kamal deployment
hosts: web
become: true
vars:
swap_file_size_mb: "2048"
timezone: "UTC"
ufw_allowed_ports: [22, 80, 443]
roles:
- role: geerlingguy.swap
when: ansible_swaptotal_mb < 1
tasks:
# System updates
- name: Update and upgrade packages
ansible.builtin.apt:
update_cache: true
upgrade: dist
# Remove bloat
- name: Remove snapd
ansible.builtin.apt:
name: snapd
state: absent
purge: true
ignore_errors: true
# Essential packages
- name: Install required packages
ansible.builtin.apt:
name: [curl, ca-certificates, fail2ban, ufw, ntp]
state: present
# Docker
- name: Install Docker
ansible.builtin.shell: curl -fsSL https://get.docker.com | sh
args:
creates: /usr/bin/docker
- name: Enable Docker
ansible.builtin.systemd:
name: docker
state: started
enabled: true
# Security
- name: Configure fail2ban
ansible.builtin.copy:
dest: /etc/fail2ban/jail.local
content: |
[sshd]
enabled = true
maxretry = 5
bantime = 3600
mode: "0644"
notify: Restart fail2ban
- name: Configure UFW
community.general.ufw:
rule: allow
port: "{{ item }}"
proto: tcp
loop: "{{ ufw_allowed_ports }}"
- name: Enable UFW
community.general.ufw:
state: enabled
policy: deny
direction: incoming
# SSH hardening
- name: Harden SSH
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
loop:
- { regexp: "^#?PasswordAuthentication", line: "PasswordAuthentication no" }
- { regexp: "^#?PermitRootLogin", line: "PermitRootLogin prohibit-password" }
notify: Restart ssh
# Performance
- name: Tune kernel
ansible.posix.sysctl:
name: "{{ item.name }}"
value: "{{ item.value }}"
reload: true
loop:
- { name: vm.swappiness, value: "10" }
- { name: net.core.somaxconn, value: "65535" }
- name: Set timezone
community.general.timezone:
name: "{{ timezone }}"
handlers:
- name: Restart fail2ban
ansible.builtin.systemd:
name: fail2ban
state: restarted
- name: Restart ssh
ansible.builtin.systemd:
name: ssh
state: restarted
#!/usr/bin/env bash
# infra/bin/provision
# 1. Terraform creates server
cd infra && tofu apply
SERVER_IP=$(tofu output -raw server_ip)
# 2. Wait for SSH
until ssh -o ConnectTimeout=5 root@$SERVER_IP true 2>/dev/null; do
sleep 5
done
# 3. Generate inventory
echo "[web]\n$SERVER_IP ansible_user=root" > ansible/hosts.ini
# 4. Run Ansible
cd ansible
ansible-galaxy install -r requirements.yml
ANSIBLE_HOST_KEY_CHECKING=False ansible-playbook -i hosts.ini playbook.yml
# 5. Kamal bootstrap
cd ../..
bundle exec kamal server bootstrap
| Issue | Cause | Fix |
|---|---|---|
ssh: connect refused | Server not ready | Wait or check firewall |
Permission denied | Wrong SSH key | Specify with -i |
sudo: password required | User needs NOPASSWD | Use become_method: sudo |
| Handler not running | Task didn't change | Use changed_when: true |
| Module not found | Missing collection | Install from requirements.yml |
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.