Files
James Bland befb8fbaeb 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>
2026-01-20 15:47:34 -05:00

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

# 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 }}"