← Tech Guides

Ansible

Sustainable Infrastructure Automation - Configuration management and orchestration for the future

Section 01

Quick Reference

Essential commands and configuration for daily Ansible usage

Essential Commands

# Ad-hoc commands
ansible <host-pattern> -m <module> -a "<arguments>"
ansible all -m ping
ansible webservers -m shell -a "uptime"
ansible all -m copy -a "src=/etc/hosts dest=/tmp/hosts"

# Playbook execution
ansible-playbook playbook.yml
ansible-playbook -i inventory.ini playbook.yml
ansible-playbook playbook.yml --limit webservers
ansible-playbook playbook.yml --tags "install,config"
ansible-playbook playbook.yml --skip-tags "testing"
ansible-playbook playbook.yml -e "version=1.2.3"

# Check and diff modes
ansible-playbook playbook.yml --check        # Dry run
ansible-playbook playbook.yml --diff         # Show diffs
ansible-playbook playbook.yml --check --diff # Both

# Syntax validation
ansible-playbook playbook.yml --syntax-check
ansible-playbook playbook.yml --list-hosts
ansible-playbook playbook.yml --list-tasks
ansible-playbook playbook.yml --list-tags

Vault Operations

# Vault operations
ansible-vault encrypt secrets.yml
ansible-vault decrypt secrets.yml
ansible-vault edit secrets.yml
ansible-vault view secrets.yml
ansible-vault rekey secrets.yml

# Playbook with vault
ansible-playbook playbook.yml --ask-vault-pass
ansible-playbook playbook.yml --vault-password-file ~/.vault_pass

Galaxy Commands

# Galaxy commands
ansible-galaxy role init myrole
ansible-galaxy role install username.rolename
ansible-galaxy collection install community.general
ansible-galaxy install -r requirements.yml

Common ansible.cfg Settings

[defaults]
inventory = ./inventory
remote_user = ansible
host_key_checking = False
retry_files_enabled = False
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts
forks = 10
timeout = 30

[privilege_escalation]
become = True
become_method = sudo
become_user = root
become_ask_pass = False

[ssh_connection]
pipelining = True
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
Section 02

Inventory

Managing hosts, groups, and inventory structure

Static Inventory (INI Format)

# inventory.ini

# Individual hosts
web1.example.com
web2.example.com

# Grouped hosts
[webservers]
web1.example.com
web2.example.com
web3.example.com

[databases]
db1.example.com
db2.example.com

# Host with connection variables
[appservers]
app1.example.com ansible_host=10.0.0.5 ansible_port=2222

# Range patterns
[webfarm]
web[01:20].example.com

# Nested groups
[production:children]
webservers
databases

# Group variables
[webservers:vars]
http_port=80
max_clients=200

[production:vars]
ansible_user=deploy
ansible_become=yes

Static Inventory (YAML Format)

---
# inventory.yml
all:
  children:
    webservers:
      hosts:
        web1.example.com:
          ansible_host: 192.168.1.10
        web2.example.com:
          ansible_host: 192.168.1.11
      vars:
        http_port: 80
        max_clients: 200

    databases:
      hosts:
        db1.example.com:
        db2.example.com:
      vars:
        db_port: 5432

    production:
      children:
        webservers:
        databases:
      vars:
        ansible_user: deploy
        ansible_become: yes

Directory Structure

inventory/
├── hosts                    # Main inventory file
├── host_vars/
│   ├── web1.example.com.yml # Variables for specific host
│   └── db1.example.com.yml
└── group_vars/
    ├── all.yml              # Variables for all hosts
    ├── webservers.yml       # Variables for webservers group
    └── production.yml       # Variables for production group

group_vars/all.yml:

---
ntp_server: ntp.example.com
dns_servers:
  - 8.8.8.8
  - 8.8.4.4

host_vars/web1.example.com.yml:

---
nginx_worker_processes: 4
app_environment: production
ssl_certificate: /etc/ssl/certs/web1.crt

Host Patterns

Pattern Description
all All hosts
web1.example.com Single host
webservers Group
webservers:databases Multiple groups (OR)
webservers:&production Intersection (AND)
webservers:!staging Exclusion (NOT)
web*.example.com Wildcard
webservers:&production:!web1.example.com Complex pattern
Section 03

Ad-Hoc Commands

Quick one-off tasks without writing playbooks

Syntax

ansible <host-pattern> -m <module> -a "<module-arguments>" [options]

Common Modules

Ping module:

# Test connectivity
ansible all -m ping
ansible webservers -m ping

Shell module:

# Execute shell commands (supports pipes, redirects)
ansible all -m shell -a "uptime"
ansible webservers -m shell -a "ps aux | grep nginx"
ansible all -m shell -a "echo $TERM"

Command module:

# Execute commands (no shell features)
ansible all -m command -a "uptime"
ansible all -m command -a "df -h"
ansible all -m command -a "/usr/bin/foo" -a "creates=/path/file"

Copy module:

# Copy files to remote hosts
ansible webservers -m copy -a "src=/etc/hosts dest=/tmp/hosts"
ansible all -m copy -a "src=/app/config.yml dest=/etc/app/config.yml owner=app group=app mode=0644"
ansible all -m copy -a "content='Hello World\n' dest=/tmp/hello.txt"

File module:

# Manage files and directories
ansible all -m file -a "path=/tmp/test state=touch"
ansible all -m file -a "path=/tmp/testdir state=directory mode=0755"
ansible all -m file -a "path=/tmp/test state=absent"
ansible all -m file -a "src=/etc/hosts dest=/tmp/hosts state=link"

Package management:

# Apt (Debian/Ubuntu)
ansible webservers -m apt -a "name=nginx state=present" -b
ansible all -m apt -a "name=git state=latest" -b
ansible all -m apt -a "update_cache=yes" -b

# Yum (RHEL/CentOS)
ansible webservers -m yum -a "name=nginx state=present" -b
ansible all -m yum -a "name=httpd state=latest" -b

Service module:

# Service management
ansible webservers -m service -a "name=nginx state=started" -b
ansible webservers -m service -a "name=nginx state=restarted" -b
ansible webservers -m service -a "name=nginx enabled=yes" -b

User module:

# User management
ansible all -m user -a "name=deploy state=present" -b
ansible all -m user -a "name=deploy shell=/bin/bash groups=sudo append=yes" -b
ansible all -m user -a "name=deploy state=absent remove=yes" -b

Command Options

Option Description
-i INVENTORY Specify inventory file
-u USERNAME Remote user
-b Become (sudo)
-K Ask for become password
-k Ask for SSH password
-f FORKS Number of parallel processes
-e "var=value" Extra variables
--limit SUBSET Limit to specific hosts
Section 04

Playbooks

Declarative automation with YAML playbooks

Basic Playbook Structure

---
# webserver.yml
- name: Configure web servers
  hosts: webservers
  become: yes
  vars:
    http_port: 80
    max_clients: 200

  tasks:
    - name: Install nginx
      apt:
        name: nginx
        state: present
        update_cache: yes

    - name: Start nginx service
      service:
        name: nginx
        state: started
        enabled: yes

Handlers and Notify

---
- name: Configure nginx
  hosts: webservers
  become: yes

  tasks:
    - name: Install nginx
      apt:
        name: nginx
        state: present

    - name: Copy nginx config
      copy:
        src: files/nginx.conf
        dest: /etc/nginx/nginx.conf
      notify:
        - Restart nginx
        - Check nginx status

    - name: Copy site config
      template:
        src: templates/site.conf.j2
        dest: /etc/nginx/sites-available/default
      notify: Restart nginx

  handlers:
    - name: Restart nginx
      service:
        name: nginx
        state: restarted

    - name: Check nginx status
      command: nginx -t

Tags

---
- name: Complete application setup
  hosts: appservers
  become: yes

  tasks:
    - name: Install packages
      apt:
        name: "{{ item }}"
        state: present
      loop:
        - git
        - python3-pip
      tags:
        - install
        - packages

    - name: Copy application config
      template:
        src: app.conf.j2
        dest: /etc/app/config.conf
      tags:
        - config

    - name: Run database migrations
      command: /opt/app/migrate.sh
      tags:
        - deploy
        - database

    - name: Restart application
      service:
        name: myapp
        state: restarted
      tags:
        - deploy
        - never  # Only runs when explicitly called

Run with tags:

ansible-playbook app.yml --tags "install"
ansible-playbook app.yml --tags "config,deploy"
ansible-playbook app.yml --skip-tags "database"
ansible-playbook app.yml --tags "never"

Conditionals (when)

---
- name: Conditional tasks
  hosts: all
  become: yes

  tasks:
    - name: Install Apache (Debian/Ubuntu)
      apt:
        name: apache2
        state: present
      when: ansible_os_family == "Debian"

    - name: Install Apache (RHEL/CentOS)
      yum:
        name: httpd
        state: present
      when: ansible_os_family == "RedHat"

    - name: Shut down Debian 10 systems
      command: /sbin/shutdown -t now
      when:
        - ansible_os_family == "Debian"
        - ansible_distribution_major_version == "10"

Loops

---
- name: Loop examples
  hosts: webservers
  become: yes

  tasks:
    # Simple loop
    - name: Install packages
      apt:
        name: "{{ item }}"
        state: present
      loop:
        - nginx
        - git
        - vim

    # Loop with dictionaries
    - name: Create users
      user:
        name: "{{ item.name }}"
        state: present
        groups: "{{ item.groups }}"
      loop:
        - { name: 'alice', groups: 'sudo' }
        - { name: 'bob', groups: 'developers' }
        - { name: 'charlie', groups: 'developers,docker' }

    # Loop with conditional
    - name: Install packages on Debian only
      apt:
        name: "{{ item }}"
        state: present
      loop:
        - apache2
        - php
      when: ansible_os_family == "Debian"
Section 05

Variables & Facts

Managing data and system information in Ansible

Variable Definition

---
- name: Variable examples
  hosts: webservers

  # Play-level variables
  vars:
    http_port: 80
    app_version: "1.2.3"
    packages:
      - nginx
      - git
      - vim

  # Variables from files
  vars_files:
    - vars/common.yml
    - vars/{{ ansible_os_family }}.yml

  # Prompt for variables
  vars_prompt:
    - name: deploy_version
      prompt: "Which version to deploy?"
      private: no

    - name: db_password
      prompt: "Database password?"
      private: yes

  tasks:
    - name: Show variables
      debug:
        msg: "Port: {{ http_port }}, Version: {{ app_version }}"

Variable Precedence

Variable Priority (Low to High)
  1. Command line values (lowest)
  2. Role defaults
  3. Inventory file or script group vars
  4. Inventory group_vars/all
  5. Playbook group_vars/all
  6. Inventory group_vars/*
  7. Playbook group_vars/*
  8. Inventory host vars
  9. Inventory host_vars/*
  10. Playbook host_vars/*
  11. Host facts / cached set_facts
  12. Play vars
  13. Play vars_files
  14. Role vars
  15. Block vars
  16. Task vars
  17. Set_facts / registered vars
  18. Extra vars (highest - always win)

Register Variables

---
- name: Register examples
  hosts: webservers

  tasks:
    - name: Check if file exists
      stat:
        path: /etc/app/config.conf
      register: config_file

    - name: Show file info
      debug:
        msg: "File exists: {{ config_file.stat.exists }}"

    - name: Copy config if missing
      copy:
        src: files/config.conf
        dest: /etc/app/config.conf
      when: not config_file.stat.exists

    - name: Get service status
      command: systemctl status nginx
      register: nginx_status
      ignore_errors: yes

    - name: Show nginx status
      debug:
        var: nginx_status.stdout_lines

set_fact Module

---
- name: Set fact examples
  hosts: all

  tasks:
    - name: Set deployment timestamp
      set_fact:
        deploy_timestamp: "{{ ansible_date_time.iso8601 }}"

    - name: Calculate total memory
      set_fact:
        total_memory_gb: "{{ (ansible_memtotal_mb / 1024) | round(2) }}"

    - name: Build complex fact
      set_fact:
        server_info:
          hostname: "{{ ansible_hostname }}"
          os: "{{ ansible_distribution }}"
          version: "{{ ansible_distribution_version }}"
          ip: "{{ ansible_default_ipv4.address }}"

    - name: Use facts
      debug:
        msg: "Deployed at {{ deploy_timestamp }} on {{ server_info.hostname }}"

Common Ansible Facts

Fact Description
ansible_hostname Short hostname
ansible_fqdn Fully qualified domain name
ansible_os_family Debian, RedHat, etc.
ansible_distribution Ubuntu, CentOS, etc.
ansible_distribution_version OS version number
ansible_processor_vcpus Number of CPUs
ansible_memtotal_mb Total memory in MB
ansible_default_ipv4.address Primary IP address
ansible_date_time.iso8601 Current timestamp

Magic Variables

---
- name: Magic variables
  hosts: all

  tasks:
    - name: Show inventory info
      debug:
        msg: |
          Inventory hostname: {{ inventory_hostname }}
          Groups: {{ group_names }}
          All groups: {{ groups }}
          Play hosts: {{ ansible_play_hosts }}

    - name: Access hostvars
      debug:
        msg: "Web1 IP: {{ hostvars['web1.example.com'].ansible_default_ipv4.address }}"

    - name: Loop through group
      debug:
        msg: "Host {{ item }} in webservers"
      loop: "{{ groups['webservers'] }}"
Section 06

Templates & Files

Dynamic configuration with Jinja2 templates and file management

Jinja2 Templates

templates/nginx.conf.j2:

# Nginx configuration
user {{ nginx_user }};
worker_processes {{ nginx_worker_processes }};
pid /run/nginx.pid;

events {
    worker_connections {{ nginx_worker_connections }};
}

http {
    sendfile on;
    keepalive_timeout {{ nginx_keepalive_timeout }};

    # Conditional content
    {% if nginx_gzip_enabled %}
    gzip on;
    gzip_vary on;
    gzip_types text/plain text/css application/json;
    {% endif %}

    # Loop through upstream servers
    upstream backend {
        {% for server in backend_servers %}
        server {{ server.host }}:{{ server.port }} weight={{ server.weight }};
        {% endfor %}
    }

    include /etc/nginx/sites-enabled/*;
}

Using the template:

---
- name: Configure nginx
  hosts: webservers
  become: yes

  vars:
    nginx_user: www-data
    nginx_worker_processes: auto
    nginx_worker_connections: 1024
    nginx_keepalive_timeout: 65
    nginx_gzip_enabled: true
    backend_servers:
      - { host: '10.0.1.10', port: 8080, weight: 3 }
      - { host: '10.0.1.11', port: 8080, weight: 2 }
      - { host: '10.0.1.12', port: 8080, weight: 1 }

  tasks:
    - name: Deploy nginx config
      template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/nginx.conf
        owner: root
        group: root
        mode: '0644'
        backup: yes
        validate: nginx -t -c %s
      notify: Restart nginx

Jinja2 Filters

Filter Example Result
lower "HELLO" | lower hello
upper "hello" | upper HELLO
default undefined_var | default('fallback') fallback
length [1,2,3] | length 3
join [1,2,3] | join(',') 1,2,3
to_json my_dict | to_json JSON string
to_yaml my_dict | to_yaml YAML string
basename '/path/to/file.txt' | basename file.txt
dirname '/path/to/file.txt' | dirname /path/to

File Module

---
- name: File management
  hosts: webservers
  become: yes

  tasks:
    # Create file
    - name: Create empty file
      file:
        path: /tmp/testfile
        state: touch
        owner: www-data
        group: www-data
        mode: '0644'

    # Create directory
    - name: Create directory
      file:
        path: /opt/myapp/config
        state: directory
        owner: deploy
        group: deploy
        mode: '0755'
        recurse: yes

    # Create symlink
    - name: Create symbolic link
      file:
        src: /opt/myapp/current
        dest: /opt/myapp/app
        state: link

    # Delete file/directory
    - name: Remove file
      file:
        path: /tmp/testfile
        state: absent

Lineinfile Module

---
- name: Lineinfile examples
  hosts: all
  become: yes

  tasks:
    # Add line if not present
    - name: Ensure line in sudoers
      lineinfile:
        path: /etc/sudoers
        line: 'deploy ALL=(ALL) NOPASSWD: ALL'
        validate: '/usr/sbin/visudo -cf %s'

    # Replace line
    - name: Set hostname
      lineinfile:
        path: /etc/hostname
        regexp: '^.*$'
        line: 'webserver01'

    # Remove line
    - name: Remove line
      lineinfile:
        path: /etc/hosts
        regexp: '^192\.168\.1\.100'
        state: absent

Blockinfile Module

---
- name: Blockinfile examples
  hosts: all
  become: yes

  tasks:
    # Insert block
    - name: Add SSH config block
      blockinfile:
        path: /etc/ssh/sshd_config
        block: |
          Match User ansible
              PasswordAuthentication no
              PubkeyAuthentication yes
      notify: Restart sshd

    # Add hosts entries
    - name: Add hosts entries
      blockinfile:
        path: /etc/hosts
        insertafter: EOF
        block: |
          192.168.1.10 web1.example.com
          192.168.1.11 web2.example.com
          192.168.1.12 db1.example.com
Section 07

Roles & Collections

Organizing and sharing reusable automation content

Role Directory Structure

roles/
└── nginx/
    ├── README.md
    ├── defaults/
    │   └── main.yml        # Default variables (lowest precedence)
    ├── files/
    │   └── nginx.conf      # Static files for copy module
    ├── handlers/
    │   └── main.yml        # Handlers
    ├── meta/
    │   └── main.yml        # Role metadata and dependencies
    ├── tasks/
    │   ├── main.yml        # Main task file
    │   ├── install.yml     # Task includes
    │   └── configure.yml
    ├── templates/
    │   └── site.conf.j2    # Jinja2 templates
    ├── tests/
    │   ├── inventory
    │   └── test.yml        # Test playbook
    └── vars/
        └── main.yml        # Role variables (high precedence)

Creating a Role

# Initialize role structure
ansible-galaxy role init nginx
ansible-galaxy role init roles/myapp

# Custom skeleton
ansible-galaxy role init --init-path roles/ --offline myapp

Role Example

roles/nginx/defaults/main.yml:

---
nginx_port: 80
nginx_worker_processes: auto
nginx_worker_connections: 1024
nginx_remove_default_site: true
nginx_sites:
  - name: default
    server_name: localhost
    root: /var/www/html

roles/nginx/tasks/main.yml:

---
- name: Include OS-specific variables
  include_vars: "{{ ansible_os_family }}.yml"

- name: Install nginx
  include_tasks: install.yml

- name: Configure nginx
  include_tasks: configure.yml

- name: Ensure nginx is running
  service:
    name: "{{ nginx_service }}"
    state: started
    enabled: yes

roles/nginx/handlers/main.yml:

---
- name: Restart nginx
  service:
    name: "{{ nginx_service }}"
    state: restarted

- name: Reload nginx
  service:
    name: "{{ nginx_service }}"
    state: reloaded

Using Roles in Playbooks

---
# Simple role usage
- name: Configure web servers
  hosts: webservers
  roles:
    - nginx

# Roles with variables
- name: Configure nginx with custom port
  hosts: webservers
  roles:
    - role: nginx
      nginx_port: 8080
      nginx_worker_processes: 4

# Pre and post tasks
- name: Deploy application
  hosts: appservers

  pre_tasks:
    - name: Take server out of load balancer
      command: /usr/local/bin/remove_from_lb.sh

  roles:
    - nginx
    - application

  post_tasks:
    - name: Add server back to load balancer
      command: /usr/local/bin/add_to_lb.sh

Requirements.yml

---
# Roles from Galaxy
roles:
  - name: geerlingguy.nginx
    version: 3.1.4

  - name: geerlingguy.mysql

  - src: https://github.com/user/ansible-role-custom.git
    name: custom
    version: master

# Collections from Galaxy
collections:
  - name: community.general
    version: ">=4.0.0"

  - name: ansible.posix
    version: 1.5.4

  - name: community.mysql

Installing:

# Install roles
ansible-galaxy role install -r requirements.yml

# Install collections
ansible-galaxy collection install -r requirements.yml

# Force reinstall
ansible-galaxy collection install -r requirements.yml --force

Using Collections

---
- name: Using collections
  hosts: all

  collections:
    - community.general
    - ansible.posix

  tasks:
    # Fully qualified collection name (FQCN)
    - name: Use docker_container module
      community.docker.docker_container:
        name: myapp
        image: nginx
        state: started

    # Short name (when collection is imported)
    - name: Manage firewall
      firewalld:
        port: 80/tcp
        permanent: yes
        state: enabled
Section 08

Control Flow

Error handling, delegation, and execution control

Block, Rescue, Always

---
- name: Error handling with blocks
  hosts: webservers
  become: yes

  tasks:
    - name: Handle deployment with error recovery
      block:
        - name: Download application
          get_url:
            url: https://example.com/app.tar.gz
            dest: /tmp/app.tar.gz

        - name: Extract application
          unarchive:
            src: /tmp/app.tar.gz
            dest: /opt/app
            remote_src: yes

        - name: Start application
          service:
            name: myapp
            state: started

      rescue:
        - name: Log error
          debug:
            msg: "Deployment failed, rolling back"

        - name: Restore from backup
          command: /usr/local/bin/restore_backup.sh

      always:
        - name: Cleanup temp files
          file:
            path: /tmp/app.tar.gz
            state: absent

Delegate_to

---
- name: Delegation examples
  hosts: webservers

  tasks:
    # Run task on different host
    - name: Add server to monitoring
      command: /usr/local/bin/add_to_monitoring.sh {{ inventory_hostname }}
      delegate_to: monitoring.example.com

    # Run on localhost (control node)
    - name: Check API availability
      uri:
        url: https://api.example.com/health
        return_content: yes
      delegate_to: localhost
      register: api_status

    # Local action (shortcut)
    - name: Download file locally
      local_action:
        module: get_url
        url: https://example.com/file.txt
        dest: /tmp/file.txt

Run_once

---
- name: Run once examples
  hosts: webservers

  tasks:
    # Run only on first host
    - name: Run database migration (once)
      command: /opt/app/migrate.sh
      run_once: yes

    # Run once with delegation
    - name: Update DNS
      command: /usr/local/bin/update_dns.sh
      delegate_to: dns.example.com
      run_once: yes

Serial (Rolling Updates)

---
- name: Rolling deployment
  hosts: webservers
  serial: 2  # Process 2 hosts at a time

  tasks:
    - name: Remove from load balancer
      command: /usr/local/bin/remove_from_lb.sh {{ inventory_hostname }}
      delegate_to: loadbalancer.example.com

    - name: Deploy new version
      copy:
        src: app-v2.tar.gz
        dest: /opt/app/

    - name: Restart application
      service:
        name: myapp
        state: restarted

    - name: Wait for application
      wait_for:
        port: 8080
        delay: 5
        timeout: 60

    - name: Add back to load balancer
      command: /usr/local/bin/add_to_lb.sh {{ inventory_hostname }}
      delegate_to: loadbalancer.example.com
Serial Options

You can use percentages (serial: "20%") or multiple batches (serial: [1, 25%, 100%]) for flexible rolling updates.

Include and Import Tasks

---
# import_tasks (static, processed at parse time)
- name: Import tasks example
  hosts: all

  tasks:
    - name: Import installation tasks
      import_tasks: tasks/install.yml

    - name: Import with variables
      import_tasks: tasks/configure.yml
      vars:
        config_file: /etc/app/prod.conf

# include_tasks (dynamic, processed at runtime)
- name: Include tasks example
  hosts: all

  tasks:
    - name: Include tasks conditionally
      include_tasks: tasks/database.yml
      when: setup_database

    - name: Include tasks in loop
      include_tasks: tasks/create_user.yml
      loop:
        - alice
        - bob
        - charlie
      loop_control:
        loop_var: username
Section 09

Vault & Secrets

Encrypting sensitive data with Ansible Vault

Basic Vault Operations

# Create encrypted file
ansible-vault create secrets.yml

# Encrypt existing file
ansible-vault encrypt vars/passwords.yml

# Decrypt file
ansible-vault decrypt secrets.yml

# Edit encrypted file (opens in editor)
ansible-vault edit secrets.yml

# View encrypted file (read-only)
ansible-vault view secrets.yml

# Change vault password (rekey)
ansible-vault rekey secrets.yml

# Encrypt specific string
ansible-vault encrypt_string 'mysecretpassword' --name 'db_password'
ansible-vault encrypt_string --stdin-name 'api_key'

Using Encrypted Variables

vars/secrets.yml (encrypted):

---
db_password: "supersecret"
api_key: "abc123xyz789"
ssl_key_password: "keypass123"

Encrypt it:

ansible-vault encrypt vars/secrets.yml

Using in playbook:

---
- name: Deploy application
  hosts: appservers
  vars_files:
    - vars/secrets.yml

  tasks:
    - name: Configure database connection
      template:
        src: db_config.j2
        dest: /etc/app/db.conf
      # Template can use {{ db_password }}

Run with vault:

# Prompt for password
ansible-playbook deploy.yml --ask-vault-pass

# Use password file
ansible-playbook deploy.yml --vault-password-file ~/.vault_pass

Vault Password File

~/.vault_pass:

myVaultPassword123
chmod 600 ~/.vault_pass

ansible.cfg:

[defaults]
vault_password_file = ~/.vault_pass

Encrypting Specific Strings

# Encrypt string interactively
ansible-vault encrypt_string 'secretvalue' --name 'my_secret'

# Output:
my_secret: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          66386439653765...

vars/passwords.yml:

---
# Plain text variables
db_host: db.example.com
db_port: 5432

# Encrypted password
db_password: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          66386439653765653938383866383764623066303764326436613631326634653034366361393834
          3439656537653036383766626239646662363431613134310a383238356266663061363261626635

Multiple Vault Passwords (Vault IDs)

# Create with vault ID
ansible-vault create --vault-id dev@prompt secrets_dev.yml
ansible-vault create --vault-id prod@prompt secrets_prod.yml

# Or use password files
ansible-vault create --vault-id dev@~/.vault_pass_dev secrets_dev.yml

# Run with multiple vault IDs
ansible-playbook deploy.yml --vault-id dev@prompt --vault-id prod@prompt

Best Practices

Vault Best Practices
  • Don't encrypt entire files - encrypt specific sensitive values
  • Keep encrypted variables separate from plain text variables
  • Use vault IDs for different environments (dev, staging, prod)
  • Store vault passwords securely (password managers, AWS Secrets Manager)
  • Use ansible-vault rekey regularly to rotate passwords
Section 10

Advanced Patterns

Dynamic inventory, callbacks, and advanced techniques

Dynamic Inventory Script

inventory/ec2.py:

#!/usr/bin/env python3
import json
import boto3

def get_inventory():
    ec2 = boto3.client('ec2', region_name='us-east-1')
    response = ec2.describe_instances(
        Filters=[{'Name': 'instance-state-name', 'Values': ['running']}]
    )

    inventory = {
        'all': {'hosts': []},
        '_meta': {'hostvars': {}}
    }

    for reservation in response['Reservations']:
        for instance in reservation['Instances']:
            hostname = instance.get('PublicDnsName') or instance['InstanceId']
            inventory['all']['hosts'].append(hostname)

            # Add to groups based on tags
            tags = {tag['Key']: tag['Value'] for tag in instance.get('Tags', [])}

            if 'Environment' in tags:
                env = tags['Environment']
                if env not in inventory:
                    inventory[env] = {'hosts': []}
                inventory[env]['hosts'].append(hostname)

            # Set host variables
            inventory['_meta']['hostvars'][hostname] = {
                'ansible_host': instance.get('PublicIpAddress'),
                'instance_type': instance['InstanceType'],
                'instance_id': instance['InstanceId']
            }

    return inventory

if __name__ == '__main__':
    import sys
    if len(sys.argv) == 2 and sys.argv[1] == '--list':
        print(json.dumps(get_inventory(), indent=2))
    elif len(sys.argv) == 3 and sys.argv[1] == '--host':
        print(json.dumps({}))
    else:
        print(json.dumps({}))

Inventory Plugins

inventory/aws_ec2.yml:

---
plugin: amazon.aws.aws_ec2
regions:
  - us-east-1
  - us-west-2
filters:
  instance-state-name: running
keyed_groups:
  - key: tags.Environment
    prefix: env
  - key: tags.Application
    prefix: app
  - key: instance_type
    prefix: type
hostnames:
  - dns-name
  - private-ip-address
compose:
  ansible_host: public_ip_address

Use it:

ansible-inventory -i inventory/aws_ec2.yml --list
ansible-playbook -i inventory/aws_ec2.yml playbook.yml

Callback Plugins

ansible.cfg:

[defaults]
callback_whitelist = profile_tasks, timer, yaml

# Enable custom callback
callback_plugins = ./plugins/callback
Built-in Callbacks
  • profile_tasks - Show execution time for each task
  • timer - Show total playbook execution time
  • yaml - YAML output format
  • json - JSON output format

Ansible-pull

# Setup cron job on managed nodes to pull and run playbooks
ansible-pull -U https://github.com/user/ansible-repo.git -i hosts local.yml

# With specific branch
ansible-pull -U https://github.com/user/ansible-repo.git -C production local.yml

# Run every 30 minutes
*/30 * * * * ansible-pull -U https://github.com/user/ansible-repo.git -o local.yml

local.yml (in git repo):

---
- name: Local configuration
  hosts: localhost
  connection: local
  become: yes

  tasks:
    - name: Ensure packages are installed
      package:
        name:
          - vim
          - git
        state: present

    - name: Pull latest app code
      git:
        repo: https://github.com/company/app.git
        dest: /opt/app
        version: main

Strategy Plugins

ansible.cfg:

[defaults]
strategy = free  # or linear (default), debug

In playbook:

---
- name: Fast deployment
  hosts: webservers
  strategy: free  # Don't wait for all hosts to complete each task

  tasks:
    - name: Deploy application
      copy:
        src: app.tar.gz
        dest: /opt/app/
Section 11

Pro Tips

Best practices and performance optimization

Idempotency Best Practices

---
# BAD - Always reports changed
- name: Append to file (bad)
  shell: echo "line" >> /etc/config

# GOOD - Idempotent
- name: Ensure line in file (good)
  lineinfile:
    path: /etc/config
    line: "line"
    state: present

# Use creates/removes with command module
- name: Extract archive
  command: tar xzf /tmp/app.tar.gz -C /opt/app
  args:
    creates: /opt/app/app.bin

# Use changed_when to control change status
- name: Check service status
  command: systemctl status nginx
  register: nginx_status
  changed_when: false
  failed_when: false

Check Mode (Dry Run)

---
- name: Check mode support
  hosts: all

  tasks:
    # This task supports check mode
    - name: Install package
      apt:
        name: nginx
        state: present

    # Force task to run in check mode
    - name: Always check
      command: /usr/bin/check_script.sh
      check_mode: yes

    # Never run in check mode (always execute)
    - name: Always run
      command: /usr/bin/must_run.sh
      check_mode: no

    # Custom check mode behavior
    - name: Custom check
      command: /usr/bin/deploy.sh
      when: not ansible_check_mode

Run check mode:

ansible-playbook playbook.yml --check
ansible-playbook playbook.yml -C

Debugging

---
- name: Debugging techniques
  hosts: localhost
  gather_facts: yes

  vars:
    app_version: "2.1.0"

  tasks:
    # Simple debug message
    - name: Show message
      debug:
        msg: "Deploying version {{ app_version }}"

    # Debug variable
    - name: Show variable
      debug:
        var: ansible_facts

    # Debug with verbosity control
    - name: Verbose debug
      debug:
        msg: "Only shown with -v or higher"
        verbosity: 1

    # Assert for validation
    - name: Validate configuration
      assert:
        that:
          - app_version is defined
          - ansible_distribution in ['Ubuntu', 'Debian']
        fail_msg: "Invalid configuration"
        success_msg: "Configuration valid"

Verbosity levels:

ansible-playbook playbook.yml -v      # Verbose
ansible-playbook playbook.yml -vv     # More verbose
ansible-playbook playbook.yml -vvv    # Debug (connection info)
ansible-playbook playbook.yml -vvvv   # SSH debug

Performance Tuning

ansible.cfg optimization:

[defaults]
# Increase parallel execution
forks = 20

# Disable fact gathering if not needed
gathering = explicit

# Enable fact caching
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts
fact_caching_timeout = 3600

# Reduce SSH overhead
host_key_checking = False
retry_files_enabled = False

[ssh_connection]
# Enable pipelining (requires 'requiretty' disabled in sudoers)
pipelining = True

# Reuse SSH connections
ssh_args = -o ControlMaster=auto -o ControlPersist=3600s

# Increase SSH timeout for slow networks
timeout = 30

Async and Polling

---
- name: Async task examples
  hosts: webservers

  tasks:
    # Fire and forget (poll: 0)
    - name: Long background task
      command: /usr/bin/background_task.sh
      async: 3600
      poll: 0
      register: background_job

    # Async with polling
    - name: Task with timeout
      command: /usr/bin/slow_task.sh
      async: 600    # 10 minute timeout
      poll: 10      # Check every 10 seconds

    # Run multiple async tasks
    - name: Start multiple tasks
      command: "/usr/bin/task.sh {{ item }}"
      async: 300
      poll: 0
      loop:
        - task1
        - task2
        - task3
      register: async_tasks

    # Wait for all to complete
    - name: Wait for tasks
      async_status:
        jid: "{{ item.ansible_job_id }}"
      register: async_results
      until: async_results.finished
      retries: 30
      delay: 5
      loop: "{{ async_tasks.results }}"

Error Handling

---
- name: Error handling
  hosts: all

  tasks:
    # Ignore errors
    - name: Try to stop service
      service:
        name: optional-service
        state: stopped
      ignore_errors: yes

    # Custom failure condition
    - name: Check disk space
      shell: df -h / | tail -1 | awk '{print $5}' | sed 's/%//'
      register: disk_usage
      failed_when: disk_usage.stdout|int > 90

    # Multiple failure conditions
    - name: Run command
      command: /usr/bin/check.sh
      register: check_result
      failed_when:
        - check_result.rc != 0
        - "'WARNING' not in check_result.stderr"

    # Retry on failure
    - name: Download file
      get_url:
        url: https://example.com/file.tar.gz
        dest: /tmp/file.tar.gz
      retries: 3
      delay: 5
      register: download
      until: download is success

Testing and Validation

---
- name: Testing playbook
  hosts: webservers

  tasks:
    # Validate before making changes
    - name: Check syntax of config
      command: nginx -t -c {{ config_file }}
      changed_when: false
      check_mode: no

    # Wait for service
    - name: Restart nginx
      service:
        name: nginx
        state: restarted

    - name: Wait for nginx
      wait_for:
        port: 80
        delay: 2
        timeout: 30

    # Validate service is responding
    - name: Check nginx is serving
      uri:
        url: http://localhost
        status_code: 200
      retries: 3
      delay: 5

    # Smoke tests
    - name: Run smoke tests
      command: /usr/local/bin/smoke_test.sh
      changed_when: false

Key Takeaways

Ansible Best Practices
  • Always write idempotent playbooks - they should be safe to run multiple times
  • Use roles to organize and share reusable content
  • Leverage vault for secrets management
  • Test with --check and --diff before running in production
  • Use tags for selective execution
  • Keep playbooks simple and readable - complexity is the enemy
  • Document your roles and playbooks with README files
  • Use version control for all Ansible content
  • Prefer declarative modules over shell/command when possible
  • Monitor and optimize performance with callback plugins