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>
633 lines
14 KiB
Markdown
633 lines
14 KiB
Markdown
---
|
|
name: ansible-automation
|
|
description: 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)
|
|
|
|
```ini
|
|
[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)
|
|
```yaml
|
|
# 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)
|
|
```yaml
|
|
# 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
|
|
```yaml
|
|
# 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
|
|
```yaml
|
|
# 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
|
|
```yaml
|
|
# 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
|
|
```
|
|
|
|
```yaml
|
|
# 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
|
|
```yaml
|
|
# 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
|
|
```yaml
|
|
# 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
|
|
```jinja2
|
|
{# 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
|
|
```jinja2
|
|
{# 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
|
|
```bash
|
|
# 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
|
|
```yaml
|
|
# 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
|
|
```bash
|
|
# 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
|
|
|
|
```yaml
|
|
# 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
|
|
|
|
```yaml
|
|
- 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
|
|
|
|
```yaml
|
|
# 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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```yaml
|
|
# 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 }}"
|
|
```
|