Comprehensive Claude Code guidance system with: - 5 agents: tdd-guardian, code-reviewer, security-scanner, refactor-scan, dependency-audit - 18 skills covering languages (Python, TypeScript, Rust, Go, Java, C#), infrastructure (AWS, Azure, GCP, Terraform, Ansible, Docker/K8s, Database, CI/CD), testing (TDD, UI, Browser), and patterns (Monorepo, API Design, Observability) - 3 hooks: secret detection, auto-formatting, TDD git pre-commit - Strict TDD enforcement with 80%+ coverage requirements - Multi-model strategy: Opus for planning, Sonnet for execution (opusplan) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
14 KiB
14 KiB
name, description
| name | description |
|---|---|
| ansible-automation | Ansible configuration management with playbook patterns, roles, and best practices. Use when writing Ansible playbooks, roles, or inventory configurations. |
Ansible Automation Skill
Project Structure
ansible/
├── ansible.cfg
├── inventory/
│ ├── dev/
│ │ ├── hosts.yml
│ │ └── group_vars/
│ │ ├── all.yml
│ │ └── webservers.yml
│ ├── staging/
│ └── prod/
├── playbooks/
│ ├── site.yml # Main playbook
│ ├── webservers.yml
│ ├── databases.yml
│ └── deploy.yml
├── roles/
│ ├── common/
│ ├── nginx/
│ ├── postgresql/
│ └── app/
├── group_vars/
│ └── all.yml
├── host_vars/
└── files/
Configuration (ansible.cfg)
[defaults]
inventory = inventory/dev/hosts.yml
roles_path = roles
remote_user = ec2-user
host_key_checking = False
retry_files_enabled = False
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts
fact_caching_timeout = 86400
# Security
no_log = False
display_skipped_hosts = False
[privilege_escalation]
become = True
become_method = sudo
become_user = root
become_ask_pass = False
[ssh_connection]
pipelining = True
control_path = /tmp/ansible-ssh-%%h-%%p-%%r
Inventory Patterns
YAML Inventory (recommended)
# inventory/dev/hosts.yml
all:
children:
webservers:
hosts:
web1:
ansible_host: 10.0.1.10
web2:
ansible_host: 10.0.1.11
vars:
nginx_port: 80
app_port: 8000
databases:
hosts:
db1:
ansible_host: 10.0.2.10
postgresql_version: "15"
workers:
hosts:
worker[1:3]:
ansible_host: "10.0.3.{{ item }}"
vars:
ansible_user: ec2-user
ansible_python_interpreter: /usr/bin/python3
Dynamic Inventory (AWS)
# inventory/aws_ec2.yml
plugin: amazon.aws.aws_ec2
regions:
- eu-west-2
filters:
tag:Environment: dev
instance-state-name: running
keyed_groups:
- key: tags.Role
prefix: role
- key: placement.availability_zone
prefix: az
hostnames:
- private-ip-address
compose:
ansible_host: private_ip_address
Playbook Patterns
Main Site Playbook
# playbooks/site.yml
---
- name: Configure all hosts
hosts: all
become: true
roles:
- common
- name: Configure web servers
hosts: webservers
become: true
roles:
- nginx
- app
- name: Configure databases
hosts: databases
become: true
roles:
- postgresql
Application Deployment
# playbooks/deploy.yml
---
- name: Deploy application
hosts: webservers
become: true
serial: "25%" # Rolling deployment
max_fail_percentage: 25
vars:
app_version: "{{ lookup('env', 'APP_VERSION') | default('latest') }}"
pre_tasks:
- name: Verify deployment prerequisites
ansible.builtin.assert:
that:
- app_version is defined
- app_version != ''
fail_msg: "APP_VERSION must be set"
- name: Remove from load balancer
ansible.builtin.uri:
url: "{{ lb_api_url }}/deregister"
method: POST
body:
instance_id: "{{ ansible_hostname }}"
body_format: json
delegate_to: localhost
when: lb_api_url is defined
roles:
- role: app
vars:
app_state: present
post_tasks:
- name: Wait for application health check
ansible.builtin.uri:
url: "http://localhost:{{ app_port }}/health"
status_code: 200
register: health_check
until: health_check.status == 200
retries: 30
delay: 5
- name: Add back to load balancer
ansible.builtin.uri:
url: "{{ lb_api_url }}/register"
method: POST
body:
instance_id: "{{ ansible_hostname }}"
body_format: json
delegate_to: localhost
when: lb_api_url is defined
handlers:
- name: Restart application
ansible.builtin.systemd:
name: myapp
state: restarted
daemon_reload: true
Role Structure
Role Layout
roles/app/
├── defaults/
│ └── main.yml # Default variables (lowest priority)
├── vars/
│ └── main.yml # Role variables (higher priority)
├── tasks/
│ ├── main.yml # Main task entry point
│ ├── install.yml
│ ├── configure.yml
│ └── service.yml
├── handlers/
│ └── main.yml # Handlers for notifications
├── templates/
│ ├── app.conf.j2
│ └── systemd.service.j2
├── files/
│ └── scripts/
├── meta/
│ └── main.yml # Role metadata and dependencies
└── README.md
Role Tasks
# roles/app/tasks/main.yml
---
- name: Include installation tasks
ansible.builtin.include_tasks: install.yml
tags:
- install
- name: Include configuration tasks
ansible.builtin.include_tasks: configure.yml
tags:
- configure
- name: Include service tasks
ansible.builtin.include_tasks: service.yml
tags:
- service
# roles/app/tasks/install.yml
---
- name: Create application user
ansible.builtin.user:
name: "{{ app_user }}"
system: true
shell: /bin/false
home: "{{ app_home }}"
create_home: true
- name: Create application directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ app_user }}"
group: "{{ app_group }}"
mode: "0755"
loop:
- "{{ app_home }}"
- "{{ app_home }}/releases"
- "{{ app_home }}/shared"
- "{{ app_log_dir }}"
- name: Download application artifact
ansible.builtin.get_url:
url: "{{ app_artifact_url }}/{{ app_version }}/app.tar.gz"
dest: "{{ app_home }}/releases/{{ app_version }}.tar.gz"
checksum: "sha256:{{ app_checksum }}"
register: download_result
- name: Extract application
ansible.builtin.unarchive:
src: "{{ app_home }}/releases/{{ app_version }}.tar.gz"
dest: "{{ app_home }}/releases/{{ app_version }}"
remote_src: true
when: download_result.changed
- name: Link current release
ansible.builtin.file:
src: "{{ app_home }}/releases/{{ app_version }}"
dest: "{{ app_home }}/current"
state: link
notify: Restart application
Role Handlers
# roles/app/handlers/main.yml
---
- name: Restart application
ansible.builtin.systemd:
name: "{{ app_service_name }}"
state: restarted
daemon_reload: true
- name: Reload nginx
ansible.builtin.systemd:
name: nginx
state: reloaded
Role Defaults
# roles/app/defaults/main.yml
---
app_user: myapp
app_group: myapp
app_home: /opt/myapp
app_port: 8000
app_log_dir: /var/log/myapp
app_service_name: myapp
# These should be overridden
app_version: ""
app_artifact_url: ""
app_checksum: ""
Templates (Jinja2)
Application Config
{# roles/app/templates/app.conf.j2 #}
# Application Configuration
# Managed by Ansible - DO NOT EDIT
[server]
host = {{ app_bind_host | default('0.0.0.0') }}
port = {{ app_port }}
workers = {{ app_workers | default(ansible_processor_vcpus * 2) }}
[database]
host = {{ db_host }}
port = {{ db_port | default(5432) }}
name = {{ db_name }}
user = {{ db_user }}
# Password from environment variable
password_env = DB_PASSWORD
[logging]
level = {{ app_log_level | default('INFO') }}
file = {{ app_log_dir }}/app.log
{% if app_features is defined %}
[features]
{% for feature, enabled in app_features.items() %}
{{ feature }} = {{ enabled | lower }}
{% endfor %}
{% endif %}
Systemd Service
{# roles/app/templates/systemd.service.j2 #}
[Unit]
Description={{ app_description | default('Application Service') }}
After=network.target
Wants=network-online.target
[Service]
Type=simple
User={{ app_user }}
Group={{ app_group }}
WorkingDirectory={{ app_home }}/current
ExecStart={{ app_home }}/current/bin/app serve
ExecReload=/bin/kill -HUP $MAINPID
Restart=always
RestartSec=5
# Environment
Environment="PORT={{ app_port }}"
Environment="LOG_LEVEL={{ app_log_level | default('INFO') }}"
EnvironmentFile=-{{ app_home }}/shared/.env
# Security
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths={{ app_log_dir }} {{ app_home }}/shared
[Install]
WantedBy=multi-user.target
Secrets Management with Vault
Encrypting Variables
# Create encrypted file
ansible-vault create group_vars/prod/vault.yml
# Edit encrypted file
ansible-vault edit group_vars/prod/vault.yml
# Encrypt existing file
ansible-vault encrypt group_vars/prod/secrets.yml
# Encrypt string for inline use
ansible-vault encrypt_string 'mysecret' --name 'db_password'
Vault Variables Pattern
# group_vars/prod/vault.yml (encrypted)
vault_db_password: "supersecretpassword"
vault_api_key: "api-key-here"
# group_vars/prod/vars.yml (plain, references vault)
db_password: "{{ vault_db_password }}"
api_key: "{{ vault_api_key }}"
Using Vault in Playbooks
# Run with vault password file
ansible-playbook playbooks/site.yml --vault-password-file ~/.vault_pass
# Run with vault password prompt
ansible-playbook playbooks/site.yml --ask-vault-pass
# Multiple vault IDs
ansible-playbook playbooks/site.yml \
--vault-id dev@~/.vault_pass_dev \
--vault-id prod@~/.vault_pass_prod
Idempotency Best Practices
# GOOD: Idempotent - can run multiple times safely
- name: Ensure package is installed
ansible.builtin.apt:
name: nginx
state: present
- name: Ensure service is running
ansible.builtin.systemd:
name: nginx
state: started
enabled: true
- name: Ensure configuration file exists
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
mode: "0644"
notify: Reload nginx
# BAD: Not idempotent - will fail on second run
- name: Add line to file
ansible.builtin.shell: echo "export PATH=/app/bin:$PATH" >> /etc/profile
# Use lineinfile instead!
# GOOD: Idempotent alternative
- name: Add application to PATH
ansible.builtin.lineinfile:
path: /etc/profile.d/app.sh
line: 'export PATH=/app/bin:$PATH'
create: true
mode: "0644"
Error Handling
- name: Deploy with error handling
block:
- name: Download artifact
ansible.builtin.get_url:
url: "{{ artifact_url }}"
dest: /tmp/artifact.tar.gz
- name: Extract artifact
ansible.builtin.unarchive:
src: /tmp/artifact.tar.gz
dest: /opt/app
remote_src: true
rescue:
- name: Log deployment failure
ansible.builtin.debug:
msg: "Deployment failed on {{ inventory_hostname }}"
- name: Send alert
ansible.builtin.uri:
url: "{{ slack_webhook }}"
method: POST
body:
text: "Deployment failed on {{ inventory_hostname }}"
body_format: json
delegate_to: localhost
always:
- name: Clean up temporary files
ansible.builtin.file:
path: /tmp/artifact.tar.gz
state: absent
Conditionals and Loops
# Conditional execution
- name: Install package (Debian)
ansible.builtin.apt:
name: nginx
state: present
when: ansible_os_family == "Debian"
- name: Install package (RedHat)
ansible.builtin.yum:
name: nginx
state: present
when: ansible_os_family == "RedHat"
# Loops
- name: Create users
ansible.builtin.user:
name: "{{ item.name }}"
groups: "{{ item.groups }}"
state: present
loop:
- { name: deploy, groups: [wheel, docker] }
- { name: monitoring, groups: [wheel] }
# Loop with dict
- name: Configure services
ansible.builtin.systemd:
name: "{{ item.key }}"
state: "{{ item.value.state }}"
enabled: "{{ item.value.enabled }}"
loop: "{{ services | dict2items }}"
vars:
services:
nginx:
state: started
enabled: true
postgresql:
state: started
enabled: true
Commands
# Syntax check
ansible-playbook playbooks/site.yml --syntax-check
# Dry run (check mode)
ansible-playbook playbooks/site.yml --check
# Dry run with diff
ansible-playbook playbooks/site.yml --check --diff
# Run playbook
ansible-playbook playbooks/site.yml
# Run with specific inventory
ansible-playbook -i inventory/prod/hosts.yml playbooks/site.yml
# Limit to specific hosts
ansible-playbook playbooks/site.yml --limit webservers
# Run specific tags
ansible-playbook playbooks/site.yml --tags "configure,service"
# Skip tags
ansible-playbook playbooks/site.yml --skip-tags "install"
# Extra variables
ansible-playbook playbooks/deploy.yml -e "app_version=1.2.3"
# Ad-hoc commands
ansible webservers -m ping
ansible all -m shell -a "uptime"
ansible databases -m service -a "name=postgresql state=restarted" --become
Anti-Patterns to Avoid
# BAD: Using shell when module exists
- name: Install package
ansible.builtin.shell: apt-get install -y nginx
# GOOD: Use the appropriate module
- name: Install package
ansible.builtin.apt:
name: nginx
state: present
# BAD: Hardcoded values
- name: Create user
ansible.builtin.user:
name: deploy
uid: 1001
# GOOD: Use variables
- name: Create user
ansible.builtin.user:
name: "{{ deploy_user }}"
uid: "{{ deploy_uid | default(omit) }}"
# BAD: Secrets in plain text
- name: Set database password
ansible.builtin.lineinfile:
path: /etc/app/config
line: "DB_PASSWORD=mysecret" # NEVER!
# GOOD: Use vault
- name: Set database password
ansible.builtin.lineinfile:
path: /etc/app/config
line: "DB_PASSWORD={{ vault_db_password }}"