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