feat: initial Claude Code configuration scaffold
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>
This commit is contained in:
632
.claude/skills/infrastructure/ansible/SKILL.md
Normal file
632
.claude/skills/infrastructure/ansible/SKILL.md
Normal file
@@ -0,0 +1,632 @@
|
||||
---
|
||||
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 }}"
|
||||
```
|
||||
Reference in New Issue
Block a user