ansible

Agentless IT Automation and Configuration Management

About Ansible

Ansible is a powerful, agentless automation tool for configuration management, application deployment, and task automation. Unlike other tools (Puppet, Chef), Ansible requires no agents on managed nodes - it uses SSH and Python, both already present on most Linux systems.

Why Ansible?

  • Agentless: No software to install on managed nodes
  • Simple: YAML-based playbooks, human-readable
  • Powerful: Thousands of modules for every scenario
  • Idempotent: Safe to run multiple times - only changes what's needed
  • Push-based: Control node pushes changes to managed nodes

Key Concepts:

  • Control Node: Where Ansible runs (your workstation/management server)
  • Managed Nodes: Servers/devices being configured (targets)
  • Inventory: List of managed nodes (hosts file)
  • Playbooks: YAML files describing desired state
  • Tasks: Individual actions (install package, copy file, etc.)
  • Modules: Units of code Ansible executes (apt, yum, copy, service, etc.)
  • Roles: Reusable collections of playbooks, variables, and files
  • Facts: System information gathered from managed nodes
  • Handlers: Tasks triggered by changes (restart service after config change)

Common Use Cases:

  • Configure multiple servers identically
  • Deploy applications across environments (dev, staging, prod)
  • Patch management and system updates
  • User and SSH key management
  • Security hardening and compliance
  • Orchestrate complex multi-tier deployments
  • Disaster recovery and infrastructure rebuilds
Installation and Setup
# Install Ansible on control node (Ubuntu/Debian) $ sudo apt update $ sudo apt install ansible # Install on RHEL/CentOS $ sudo yum install ansible # Or via pip (any platform) $ pip3 install ansible --user # Verify installation $ ansible --version
Common Ansible Commands
Command Description
ansible Ad-hoc command execution on managed nodes
ansible-playbook Execute playbooks (main automation command)
ansible-inventory Display or dump inventory information
ansible-vault Encrypt/decrypt sensitive data
ansible-galaxy Download and manage roles from Ansible Galaxy
ansible-doc View module documentation
ansible-config View and manage Ansible configuration
Common Playbook Options
Option Description
-i INVENTORY Specify inventory file
-l LIMIT Limit execution to specific hosts
-t TAGS Run only tasks with specific tags
--skip-tags TAGS Skip tasks with specific tags
-e VARS Extra variables (key=value)
--check Dry run - don't make changes
--diff Show file changes (with --check)
-v, -vv, -vvv Increase verbosity (more v's = more detail)
--become Execute with privilege escalation (sudo)
--ask-become-pass Prompt for sudo password
Detailed Examples

Example 1Setup Inventory and Ad-Hoc Commands

Create inventory file and run simple ad-hoc commands:

$ mkdir ~/ansible-demo && cd ~/ansible-demo $ cat > inventory << 'EOF' [webservers] web1.example.com web2.example.com [databases] db1.example.com db2.example.com [all:vars] ansible_user=admin ansible_python_interpreter=/usr/bin/python3 EOF # Test connectivity to all hosts $ ansible all -i inventory -m ping
web1.example.com | SUCCESS => { "changed": false, "ping": "pong" } web2.example.com | SUCCESS => { "changed": false, "ping": "pong" } db1.example.com | SUCCESS => { "changed": false, "ping": "pong" } db2.example.com | SUCCESS => { "changed": false, "ping": "pong" }
# Get system info from web servers $ ansible webservers -i inventory -m setup -a "filter=ansible_distribution*"

Explanation:

  • Inventory file defines managed hosts in groups
  • [webservers], [databases]: Group names
  • [all:vars]: Variables applied to all hosts
  • -m ping: Use ping module to test connectivity
  • -m setup: Gather facts (system information)
  • -a: Arguments to module (filter facts)
  • Ad-hoc commands: Quick one-off tasks without playbooks
Tip: Place inventory file in default location /etc/ansible/hosts to avoid using -i flag every time. Or set ANSIBLE_INVENTORY environment variable.

Example 2Basic Playbook - Install and Start Service

Create first playbook to install and configure a web server:

$ cat > webserver.yml << 'EOF' --- - name: Configure web servers hosts: webservers become: yes tasks: - name: Install Apache web server apt: name: apache2 state: present update_cache: yes - name: Start and enable Apache service service: name: apache2 state: started enabled: yes - name: Create index.html copy: content: | <html> <body> <h1>Welcome to {{ ansible_hostname }}</h1> </body> </html> dest: /var/www/html/index.html owner: www-data group: www-data mode: '0644' EOF # Run the playbook $ ansible-playbook -i inventory webserver.yml
PLAY [Configure web servers] *********************************** TASK [Gathering Facts] ***************************************** ok: [web1.example.com] ok: [web2.example.com] TASK [Install Apache web server] ******************************* changed: [web1.example.com] changed: [web2.example.com] TASK [Start and enable Apache service] ************************* changed: [web1.example.com] changed: [web2.example.com] TASK [Create index.html] *************************************** changed: [web1.example.com] changed: [web2.example.com] PLAY RECAP ***************************************************** web1.example.com : ok=4 changed=3 unreachable=0 failed=0 web2.example.com : ok=4 changed=3 unreachable=0 failed=0

Explanation:

  • ---: YAML file start
  • hosts: webservers: Target group from inventory
  • become: yes: Use sudo for all tasks
  • apt module: Package management (Debian/Ubuntu)
  • service module: Manage system services
  • copy module: Create or copy files
  • {{ ansible_hostname }}: Variable (Jinja2 template)
  • changed=3: Three tasks made changes
Note: Run this playbook again and you'll see "ok" instead of "changed" - this is idempotency. Ansible only makes changes when needed, making it safe to run repeatedly.

Example 3Using Variables and Templates

Create playbook with variables and Jinja2 templates:

$ cat > config-template.yml << 'EOF' --- - name: Deploy configuration with templates hosts: webservers become: yes vars: apache_port: 8080 admin_email: admin@example.com max_clients: 150 tasks: - name: Deploy Apache configuration from template template: src: apache-config.j2 dest: /etc/apache2/ports.conf owner: root group: root mode: '0644' notify: Restart Apache handlers: - name: Restart Apache service: name: apache2 state: restarted EOF $ cat > apache-config.j2 << 'EOF' # Apache Configuration # Generated by Ansible on {{ ansible_date_time.date }} Listen {{ apache_port }} <IfModule mpm_prefork_module> StartServers 5 MinSpareServers 5 MaxSpareServers 10 MaxRequestWorkers {{ max_clients }} MaxConnectionsPerChild 0 </IfModule> ServerAdmin {{ admin_email }} EOF $ ansible-playbook -i inventory config-template.yml
PLAY [Deploy configuration with templates] ********************* TASK [Deploy Apache configuration from template] *************** changed: [web1.example.com] changed: [web2.example.com] RUNNING HANDLER [Restart Apache] ******************************* changed: [web1.example.com] changed: [web2.example.com]

Explanation:

  • vars: Define variables in playbook
  • template module: Process Jinja2 templates
  • .j2 extension: Jinja2 template file
  • {{ variable }}: Insert variable value
  • notify: Trigger handler when task changes
  • handlers: Tasks run only when notified
  • Handler runs after all tasks complete
Tip: Handlers only run once, even if notified multiple times. Perfect for restart services after multiple configuration changes. They run at the end of the play unless forced earlier with meta: flush_handlers.

Example 4User Management and SSH Keys

Manage users and deploy SSH keys across servers:

$ cat > users.yml << 'EOF' --- - name: Manage users and SSH keys hosts: all become: yes vars: admin_users: - name: alice comment: "Alice Admin" ssh_key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQ... alice@laptop" - name: bob comment: "Bob Developer" ssh_key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQ... bob@laptop" tasks: - name: Create admin users user: name: "{{ item.name }}" comment: "{{ item.comment }}" groups: sudo shell: /bin/bash state: present loop: "{{ admin_users }}" - name: Deploy SSH keys authorized_key: user: "{{ item.name }}" key: "{{ item.ssh_key }}" state: present loop: "{{ admin_users }}" - name: Disable password authentication lineinfile: path: /etc/ssh/sshd_config regexp: '^PasswordAuthentication' line: 'PasswordAuthentication no' state: present notify: Restart SSH handlers: - name: Restart SSH service: name: sshd state: restarted EOF $ ansible-playbook -i inventory users.yml
TASK [Create admin users] ************************************** changed: [web1.example.com] => (item={'name': 'alice', ...}) changed: [web1.example.com] => (item={'name': 'bob', ...}) TASK [Deploy SSH keys] ***************************************** changed: [web1.example.com] => (item={'name': 'alice', ...}) changed: [web1.example.com] => (item={'name': 'bob', ...}) TASK [Disable password authentication] ************************* changed: [web1.example.com]

Explanation:

  • user module: Create and manage user accounts
  • authorized_key module: Manage SSH keys
  • lineinfile module: Modify configuration files
  • loop: Iterate over list of items
  • {{ item.name }}: Access loop item properties
  • Creates users, adds to sudo, deploys keys
  • Hardens SSH by disabling password auth
Best Practice: Store user lists in separate variable files or group_vars. This playbook shows inline vars for clarity, but production should use: vars_files: users.yml or group_vars/all.yml.

Example 5Conditional Tasks and Facts

Use conditionals to handle different operating systems:

$ cat > multi-os.yml << 'EOF' --- - name: Install packages on multiple OS families hosts: all become: yes tasks: - name: Display OS information debug: msg: "{{ ansible_distribution }} {{ ansible_distribution_version }}" - name: Install nginx on Debian/Ubuntu apt: name: nginx state: present update_cache: yes when: ansible_os_family == "Debian" - name: Install nginx on RedHat/CentOS yum: name: nginx state: present when: ansible_os_family == "RedHat" - name: Ensure nginx is running service: name: nginx state: started enabled: yes - name: Set firewall rules on RedHat systems firewalld: service: http permanent: yes state: enabled immediate: yes when: - ansible_os_family == "RedHat" - ansible_distribution_major_version|int >= 7 EOF $ ansible-playbook -i inventory multi-os.yml
TASK [Display OS information] ********************************** ok: [web1.example.com] => { "msg": "Ubuntu 22.04" } ok: [db1.example.com] => { "msg": "CentOS 8" } TASK [Install nginx on Debian/Ubuntu] ************************** changed: [web1.example.com] skipping: [db1.example.com] TASK [Install nginx on RedHat/CentOS] ************************** skipping: [web1.example.com] changed: [db1.example.com]

Explanation:

  • ansible_distribution: Fact about OS (Ubuntu, CentOS, etc.)
  • ansible_os_family: OS family (Debian, RedHat, etc.)
  • when: Conditional - task runs only if true
  • debug module: Print messages for troubleshooting
  • Different package managers for different OS families
  • Multiple conditions with list syntax
  • Facts gathered automatically at play start
Tip: View all available facts: ansible hostname -m setup. Common facts: ansible_hostname, ansible_default_ipv4.address, ansible_memtotal_mb, ansible_processor_cores. Use facts to make playbooks adapt to target systems.

Example 6Using Roles for Organization

Structure complex playbooks using roles:

$ # Create role structure $ ansible-galaxy init webserver $ tree webserver/ webserver/ ├── defaults/ │ └── main.yml ├── files/ ├── handlers/ │ └── main.yml ├── tasks/ │ └── main.yml ├── templates/ ├── vars/ │ └── main.yml └── README.md $ cat > webserver/tasks/main.yml << 'EOF' --- - name: Install web server packages apt: name: - apache2 - libapache2-mod-php state: present - name: Deploy website files copy: src: "{{ item }}" dest: /var/www/html/ with_fileglob: "../files/*" - name: Deploy configuration template: src: vhost.conf.j2 dest: /etc/apache2/sites-available/000-default.conf notify: Restart Apache EOF $ cat > webserver/handlers/main.yml << 'EOF' --- - name: Restart Apache service: name: apache2 state: restarted EOF $ cat > site.yml << 'EOF' --- - name: Deploy complete web application hosts: webservers become: yes roles: - webserver - { role: database, when: "'databases' in group_names" } EOF $ ansible-playbook -i inventory site.yml
PLAY [Deploy complete web application] ************************* TASK [webserver : Install web server packages] **************** ok: [web1.example.com] TASK [webserver : Deploy website files] *********************** changed: [web1.example.com] TASK [webserver : Deploy configuration] *********************** changed: [web1.example.com] RUNNING HANDLER [webserver : Restart Apache] ****************** changed: [web1.example.com]

Explanation:

  • ansible-galaxy init: Create role skeleton
  • Roles organize playbooks into reusable components
  • tasks/: Main task list for role
  • handlers/: Handlers for role
  • templates/: Jinja2 template files
  • files/: Static files to copy
  • defaults/: Default variables (low precedence)
  • vars/: Role variables (high precedence)
  • Roles make playbooks modular and shareable
Best Practice: Use roles for any playbook over ~50 lines or that you'll reuse. Share roles via Ansible Galaxy (ansible-galaxy install username.rolename). Keep roles single-purpose and composable.

Example 7Ansible Vault for Secrets

Encrypt sensitive data like passwords and keys:

$ # Create encrypted variable file $ ansible-vault create secrets.yml New Vault password: ******** Confirm New Vault password: ******** # Edit the file (opens in $EDITOR) # Add content: --- db_password: SuperSecretPassword123! api_key: abc123def456ghi789 admin_password: AdminPass456! $ # View encrypted file $ cat secrets.yml
$ANSIBLE_VAULT;1.1;AES256 66386439653966336266343732323066323535303538643932633836613365643464613166376432 3731373830616537393361663632616132613730346639310a383638323061383266376636633266 ...
$ cat > secure-setup.yml << 'EOF' --- - name: Setup with encrypted secrets hosts: databases become: yes vars_files: - secrets.yml tasks: - name: Create database user mysql_user: name: appuser password: "{{ db_password }}" priv: "appdb.*:ALL" state: present - name: Configure API credentials template: src: api-config.j2 dest: /etc/app/api.conf mode: '0600' EOF # Run playbook with vault password $ ansible-playbook -i inventory secure-setup.yml --ask-vault-pass # Or use password file $ echo "MyVaultPassword" > .vault_pass $ chmod 600 .vault_pass $ ansible-playbook -i inventory secure-setup.yml --vault-password-file .vault_pass

Explanation:

  • ansible-vault create: Create encrypted file
  • ansible-vault edit: Modify encrypted file
  • ansible-vault view: View encrypted file
  • vars_files: Include encrypted variables
  • --ask-vault-pass: Prompt for decryption password
  • --vault-password-file: Read password from file
  • Variables used normally in playbook
  • Encrypted at rest, decrypted during execution
Security Note: Never commit .vault_pass or unencrypted secrets to version control! Add to .gitignore. For production, use ansible-vault with CI/CD environment variables or external secret managers (HashiCorp Vault, AWS Secrets Manager).

Example 8Tags for Selective Execution

Use tags to run specific parts of playbooks:

$ cat > tagged-playbook.yml << 'EOF' --- - name: Complete server setup hosts: webservers become: yes tasks: - name: Install base packages apt: name: - vim - git - htop state: present tags: - base - packages - name: Configure firewall ufw: rule: allow port: "{{ item }}" loop: - "22" - "80" - "443" tags: - firewall - security - name: Install Apache apt: name: apache2 state: present tags: - apache - webserver - name: Deploy application git: repo: https://github.com/example/app.git dest: /var/www/app tags: - application - deploy - name: Configure monitoring copy: src: monitoring.conf dest: /etc/monitoring/ tags: - monitoring - never # Only runs when explicitly requested EOF # Run only security-related tasks $ ansible-playbook -i inventory tagged-playbook.yml --tags security # Run everything except deployment $ ansible-playbook -i inventory tagged-playbook.yml --skip-tags deploy # Run base setup and apache $ ansible-playbook -i inventory tagged-playbook.yml --tags base,apache # Run ONLY tasks tagged 'never' $ ansible-playbook -i inventory tagged-playbook.yml --tags never
PLAY [Complete server setup] *********************************** TASK [Configure firewall] ************************************** changed: [web1.example.com] => (item=22) changed: [web1.example.com] => (item=80) changed: [web1.example.com] => (item=443) PLAY RECAP ***************************************************** web1.example.com : ok=1 changed=1 unreachable=0 failed=0

Explanation:

  • tags: Label tasks for selective execution
  • --tags: Run only tasks with these tags
  • --skip-tags: Run all except these tags
  • Multiple tags per task (list format)
  • never: Special tag - only runs when explicitly called
  • Tags inheritance: apply to entire plays or roles
  • Useful for maintenance, updates, or debugging
Tip: List all available tags: ansible-playbook playbook.yml --list-tags. Common tag strategy: base, security, config, deploy, monitoring. Use always tag for tasks that should always run regardless of tag selection.

Example 9Error Handling and Debugging

Handle failures gracefully and debug playbook issues:

$ cat > error-handling.yml << 'EOF' --- - name: Demonstrate error handling hosts: webservers become: yes tasks: - name: Attempt risky operation command: /usr/bin/might-not-exist register: result ignore_errors: yes - name: Check if previous task failed debug: msg: "Previous task failed with: {{ result.stderr }}" when: result.failed - name: Try multiple package managers block: - name: Try apt first apt: name: nginx state: present rescue: - name: Fall back to yum yum: name: nginx state: present always: - name: Verify nginx is installed command: nginx -v register: nginx_version - name: Display nginx version debug: msg: "Nginx installed: {{ nginx_version.stdout }}" - name: Fail with custom message fail: msg: "Critical condition detected!" when: ansible_memfree_mb < 100 - name: Assert conditions are met assert: that: - ansible_os_family == "Debian" - ansible_distribution_version is version('20.04', '>=') fail_msg: "This playbook requires Ubuntu 20.04 or newer" success_msg: "System meets requirements" EOF # Run with verbose output for debugging $ ansible-playbook -i inventory error-handling.yml -vv # Check mode to see what would change $ ansible-playbook -i inventory error-handling.yml --check --diff
TASK [Attempt risky operation] ********************************* fatal: [web1.example.com]: FAILED! => {"msg": "/usr/bin/might-not-exist: No such file"} ...ignoring TASK [Check if previous task failed] ************************** ok: [web1.example.com] => { "msg": "Previous task failed with: /usr/bin/might-not-exist: No such file" } TASK [Try apt first] ******************************************* changed: [web1.example.com] TASK [Verify nginx is installed] ******************************* changed: [web1.example.com] TASK [Assert conditions are met] ******************************* ok: [web1.example.com] => { "msg": "System meets requirements" }

Explanation:

  • register: Save task output to variable
  • ignore_errors: yes: Continue after failure
  • block/rescue/always: Try/catch/finally pattern
  • fail module: Deliberately fail with message
  • assert module: Verify conditions
  • --check: Dry run mode (no changes)
  • --diff: Show file differences
  • -vv: Verbose output for debugging
Tip: Use --start-at-task "Task Name" to resume from specific task during debugging. Increase verbosity with more v's: -vvv shows connection details, -vvvv shows SSH debugging. Use debug module liberally during development.

Example 10Complete Multi-Tier Application Deployment

Real-world example: Deploy complete LAMP stack application:

$ cat > lamp-stack.yml << 'EOF' --- - name: Deploy LAMP Stack Application hosts: all become: yes vars_files: - vars/database.yml pre_tasks: - name: Update apt cache (Debian) apt: update_cache: yes cache_valid_time: 3600 when: ansible_os_family == "Debian" tasks: # Database tier - name: Install MySQL server apt: name: - mysql-server - python3-pymysql state: present when: "'databases' in group_names" - name: Create application database mysql_db: name: "{{ db_name }}" state: present when: "'databases' in group_names" - name: Create database user mysql_user: name: "{{ db_user }}" password: "{{ db_password }}" priv: "{{ db_name }}.*:ALL" host: "{{ item }}" state: present loop: "{{ groups['webservers'] }}" when: "'databases' in group_names" # Web tier - name: Install Apache and PHP apt: name: - apache2 - php - php-mysql - libapache2-mod-php state: present when: "'webservers' in group_names" - name: Deploy application code git: repo: https://github.com/example/app.git dest: /var/www/html/app version: main when: "'webservers' in group_names" notify: Restart Apache - name: Configure application template: src: templates/config.php.j2 dest: /var/www/html/app/config.php mode: '0644' when: "'webservers' in group_names" - name: Set correct permissions file: path: /var/www/html/app owner: www-data group: www-data recurse: yes when: "'webservers' in group_names" - name: Enable Apache modules apache2_module: name: "{{ item }}" state: present loop: - rewrite - php when: "'webservers' in group_names" notify: Restart Apache post_tasks: - name: Verify web service is responding uri: url: http://{{ ansible_default_ipv4.address }} status_code: 200 when: "'webservers' in group_names" handlers: - name: Restart Apache service: name: apache2 state: restarted EOF $ cat > templates/config.php.j2 << 'EOF' <?php // Database Configuration define('DB_HOST', '{{ groups["databases"][0] }}'); define('DB_NAME', '{{ db_name }}'); define('DB_USER', '{{ db_user }}'); define('DB_PASS', '{{ db_password }}'); // Application Settings define('APP_URL', 'http://{{ ansible_default_ipv4.address }}'); define('DEBUG', false); ?> EOF $ ansible-playbook -i inventory lamp-stack.yml --ask-vault-pass
PLAY [Deploy LAMP Stack Application] ************************** TASK [Update apt cache (Debian)] ****************************** ok: [web1.example.com] ok: [db1.example.com] TASK [Install MySQL server] ************************************ skipping: [web1.example.com] changed: [db1.example.com] TASK [Create application database] ***************************** skipping: [web1.example.com] changed: [db1.example.com] TASK [Install Apache and PHP] ********************************** changed: [web1.example.com] skipping: [db1.example.com] TASK [Deploy application code] ********************************* changed: [web1.example.com] skipping: [db1.example.com] TASK [Configure application] *********************************** changed: [web1.example.com] skipping: [db1.example.com] TASK [Verify web service is responding] *********************** ok: [web1.example.com] skipping: [db1.example.com] RUNNING HANDLER [Restart Apache] ****************************** changed: [web1.example.com] PLAY RECAP ***************************************************** web1.example.com : ok=8 changed=5 unreachable=0 failed=0 db1.example.com : ok=4 changed=3 unreachable=0 failed=0

Explanation:

  • pre_tasks: Run before main tasks
  • post_tasks: Run after main tasks and handlers
  • groups['webservers']: Access inventory groups
  • group_names: Groups current host belongs to
  • Conditional execution based on host groups
  • Multi-tier deployment in single playbook
  • Template includes data from other hosts
  • uri module: Verify HTTP endpoints
Production Pattern: This demonstrates a complete application stack deployment. In production, you'd: (1) Split into separate playbooks or roles, (2) Use dynamic inventory from cloud provider, (3) Add health checks and rollback capability, (4) Include monitoring and alerting setup, (5) Use CI/CD pipeline for automated deployment.
Related Tools and Concepts
Common Pitfalls and Solutions
Pitfall 1: Running Playbooks Without --check First

Making changes without testing can break production systems.

Solution: Always run --check --diff first to see what would change. Test in dev environment. Use tags for incremental updates. Keep playbooks idempotent so multiple runs are safe.

Pitfall 2: Ignoring Idempotency

Using command or shell modules that aren't idempotent.

Solution: Use proper modules (apt, yum, copy, template) whenever possible. If you must use command/shell, add creates: or when: conditions. Check with: ansible-playbook playbook.yml --check - should show no changes on second run.

Pitfall 3: Hardcoding Values Instead of Variables

Hardcoded IPs, paths, passwords make playbooks inflexible and insecure.

Solution: Use variables, templates, and vault. Store environment-specific vars in group_vars/ and host_vars/. Use inventory variables for host-specific data. Encrypt sensitive data with ansible-vault.

Pitfall 4: Not Using Roles for Organization

Monolithic playbooks become unmaintainable beyond ~100 lines.

Solution: Break into roles once playbooks exceed ~50 lines or contain reusable logic. Structure: roles/webserver/, roles/database/, etc. Share roles internally via git or Ansible Galaxy. Keep roles single-purpose.

Pitfall 5: Inadequate Error Handling

Playbooks fail halfway through, leaving systems in inconsistent state.

Solution: Use block/rescue/always for critical sections. Add assert to verify prerequisites. Use failed_when and changed_when for nuanced control. Implement rollback procedures for deployments.

Pitfall 6: Slow Playbook Execution

Serial execution causes long deployment times for large inventories.

Solution: Increase parallelism: forks = 20 in ansible.cfg. Use strategy: free for independent tasks. Enable SSH pipelining and ControlPersist. Use async and poll for long-running tasks. Profile with ansible-playbook --profile.

Pro Tips and Best Practices
Tip 1: Use ansible.cfg for Project Defaults

Create ansible.cfg in project root:

[defaults] inventory = inventory/production remote_user = ansible private_key_file = ~/.ssh/ansible_key host_key_checking = False forks = 20 gathering = smart fact_caching = jsonfile fact_caching_connection = /tmp/ansible_facts retry_files_enabled = False [ssh_connection] ssh_args = -o ControlMaster=auto -o ControlPersist=60s pipelining = True

Speeds up execution and sets consistent defaults across team.

Tip 2: Directory Structure for Large Projects

Standard layout for maintainability:

project/ ├── ansible.cfg ├── inventory/ │ ├── production/ │ │ ├── hosts │ │ └── group_vars/ │ └── staging/ │ ├── hosts │ └── group_vars/ ├── roles/ │ ├── webserver/ │ ├── database/ │ └── common/ ├── playbooks/ │ ├── site.yml │ ├── webservers.yml │ └── databases.yml ├── group_vars/ │ ├── all.yml │ └── webservers.yml ├── host_vars/ └── files/

Separate inventories per environment, roles in dedicated directory.

Tip 3: Use Dynamic Inventory for Cloud

Auto-discover instances from cloud providers:

# Install AWS inventory plugin $ pip install boto3 # Configure inventory plugin (inventory/aws_ec2.yml) plugin: aws_ec2 regions: - us-east-1 filters: tag:Environment: production keyed_groups: - key: tags.Role prefix: role_ # Use dynamic inventory $ ansible-playbook -i inventory/aws_ec2.yml site.yml

No manual inventory maintenance. Supports AWS, Azure, GCP, VMware, etc.

Tip 4: Ansible Testing Pipeline

Test playbooks before production:

# Syntax check $ ansible-playbook --syntax-check playbook.yml # Linting $ ansible-lint playbook.yml # Dry run $ ansible-playbook --check --diff playbook.yml # Test in staging $ ansible-playbook -i inventory/staging playbook.yml # Deploy to production $ ansible-playbook -i inventory/production playbook.yml

Catch errors early, validate in staging, deploy with confidence.

Tip 5: Document with Comments and README

Every playbook and role needs documentation:

# In playbook --- # Purpose: Deploy web application to production # Requirements: Ubuntu 20.04+, sudo access # Variables: See group_vars/webservers.yml # Tags: deploy, config, rollback # In role README.md # Webserver Role ## Requirements - Ubuntu 20.04 or CentOS 8+ - Minimum 2GB RAM ## Variables - `apache_port`: HTTP port (default: 80) - `max_clients`: Max concurrent connections (default: 150)

Future you (and your replacement) will thank you.

Tip 6: Version Control Everything

Git workflow for Ansible:

$ git init $ cat > .gitignore << 'EOF' *.retry .vault_pass .DS_Store __pycache__/ *.pyc EOF $ git add . $ git commit -m "Initial Ansible project" # Feature branch workflow $ git checkout -b feature/new-role # Make changes, test $ git commit -am "Add database role" $ git push origin feature/new-role # Create pull request, get review $ git checkout main $ git merge feature/new-role

Track changes, enable collaboration, enable rollback.

Historical Note: Ansible was created by Michael DeHaan in 2012 and acquired by Red Hat in 2015. The name comes from Ursula K. Le Guin's science fiction novels, referring to a fictional device for faster-than-light communication. Ansible's "batteries included" philosophy and agentless architecture made it extremely popular for DevOps automation, competing with Chef and Puppet despite being newer.
Quick Reference Cheat Sheet
Task Command
Test connectivity ansible all -i inventory -m ping
Run ad-hoc command ansible all -i inventory -m command -a "uptime"
Run playbook ansible-playbook -i inventory playbook.yml
Dry run (check mode) ansible-playbook playbook.yml --check --diff
Run specific tags ansible-playbook playbook.yml --tags deploy
Limit to hosts ansible-playbook playbook.yml -l web1.example.com
With sudo ansible-playbook playbook.yml --become
With vault ansible-playbook playbook.yml --ask-vault-pass
Verbose output ansible-playbook playbook.yml -vvv
View module docs ansible-doc apt
List hosts ansible all -i inventory --list-hosts
Gather facts ansible hostname -m setup