AUTOMATE
EVERYWHERE

// Configuration management without the complexity.

ANSIBLE CHANGED HOW WE THINK ABOUT SERVERS.

Imagine managing thousands of servers with a simple, human-readable languageβ€”no agents to install, no complex infrastructure to maintain. Ansible connects to your servers over SSH, executes tasks, and ensures your systems are configured exactly as specified.

WHY ANSIBLE?

Ansible uses an agentless architecture. You install Ansible on one machine (the control node), and it manages all your managed nodes via SSH. No daemon, no agent, no additional portsβ€”just SSH and Python. It's simple, powerful, and declarative.

BECOME AN AUTOMATION ENGINEER.

Learn to write Ansible playbooks, create reusable roles, manage complex infrastructure, and integrate automation into your CI/CD pipelines. Whether you're managing a handful of servers or thousands, Ansible scales with you.

BEGIN YOUR JOURNEY β†’

// The Path to Automation Mastery

12 lessons. Complete Ansible control.

LESSON 01

Introduction to Ansible

What is Ansible? Installing and configuring your first Ansible environment.

Beginner
LESSON 02

Inventory & Ad-Hoc Commands

Managing inventory, groups, variables, and running ad-hoc commands.

Beginner
LESSON 03

Playbooks Fundamentals

YAML syntax, tasks, plays, and your first playbook.

Beginner
LESSON 04

Modules Deep Dive

Common modules: file, copy, command, shell, service, and more.

Intermediate
LESSON 05

Variables & Facts

Variables, facts, registered variables, and magic variables.

Intermediate
LESSON 06

Conditionals & Loops

When conditions, loops with_loop, and template iteration.

Intermediate
LESSON 07

Roles

Creating reusable roles, role structure, and Ansible Galaxy.

Intermediate
LESSON 08

Templates

Jinja2 templating, variables in templates, and conditionals.

Intermediate
LESSON 09

Vault & Security

Encrypting sensitive data, Ansible Vault, and security best practices.

Advanced
LESSON 10

Error Handling

Error handling, blocks, rescue, always, and debugging.

Advanced
LESSON 11

Testing & CI/CD

Testing Ansible, molecule, ansible-lint, and CI/CD integration.

Advanced
LESSON 12

Enterprise Patterns

Ansible Tower/AWX, collections, and production architectures.

Advanced
← Back to Lessons

// Lesson 1: Introduction to Ansible

What is Ansible?

Ansible is an open-source automation tool developed by Red Hat. It handles configuration management, application deployment, task automation, and IT orchestration. Unlike other automation tools, Ansible is designed to be simple, readable, and agentless.

The key principles of Ansible:

How Ansible Works

Ansible follows a simple architecture:

  1. Control Node: The machine where Ansible is installed
  2. Managed Nodes: The servers you want to manage
  3. Inventory: A list of managed nodes
  4. Modules: Units of work that Ansible executes
  5. Playbooks: YAML files defining automation tasks

Ansible connects to managed nodes over SSH (or WinRM for Windows), executes modules, and then exits. No daemon runs on the managed nodes.

Installing Ansible

# macOS
brew install ansible

# Ubuntu/Debian
sudo apt update
sudo apt install ansible

# CentOS/RHEL
sudo yum install epel-release
sudo yum install ansible

# pip (works on any platform with Python)
pip install ansible

# Verify installation
ansible --version

Key Concepts

Inventory

The inventory is a file that lists your managed nodes. It can be static (a file) or dynamic (from a cloud provider):

# inventory
[webservers]
web1.example.com
web2.example.com
web3.example.com

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

[production:children]
webservers
databases

Modules

Modules are the units of work Ansible executes. There are thousands of built-in modules for almost any task:

Playbooks

Playbooks are YAML files that describe the desired state of your systems:

# site.yml
---
- name: Configure web servers
  hosts: webservers
  tasks:
    - name: Install nginx
      apt:
        name: nginx
        state: present

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

Your First Ansible Command

Let's verify Ansible is working by connecting to localhost:

# Create a simple inventory
echo "localhost" > inventory

# Test connectivity (ping module)
ansible -i inventory -m ping all

# Run a simple command
ansible -i inventory -m command -a "uptime" all
Next: Inventory & Ad-Hoc Commands β†’
← Back to Lessons

// Lesson 2: Inventory & Ad-Hoc Commands

The inventory is the foundation of Ansible. It defines what hosts you manage and how to connect to them.

Inventory File Formats

INI Format

# Basic inventory
web1.example.com
web2.example.com

[webservers]
web1.example.com
web2.example.com

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

[production:children]
webservers
databases

YAML Format

all:
  hosts:
    web1.example.com:
    web2.example.com:
  children:
    webservers:
      hosts:
        web1.example.com:
        web2.example.com:
    databases:
      hosts:
        db1.example.com:
        db2.example.com:

Inventory Variables

You can define variables directly in the inventory:

[webservers]
web1.example.com ansible_host=192.168.1.10 ansible_user=ubuntu
web2.example.com ansible_host=192.168.1.11 ansible_user=ubuntu

[webservers:vars]
ansible_python_interpreter=/usr/bin/python3
ansible_ssh_private_key_file=~/.ssh/id_rsa

Dynamic Inventory

For cloud environments, use dynamic inventory scripts:

# AWS EC2 dynamic inventory
# Download and configure ec2.py
curl -o ec2.py https://raw.githubusercontent.com/ansible/ansible/devel/contrib/inventory/ec2.py
chmod +x ec2.py

# Use it
ansible -i ec2.py -m ping all

# Or configure in ansible.cfg
# [inventory]
# enable_plugins = aws_ec2, host_list, yaml, ini

Ad-Hoc Commands

Ad-hoc commands are quick one-liners for tasks that don't need a playbook:

# Check all hosts are reachable
ansible all -i inventory -m ping

# Gather facts
ansible all -i inventory -m setup

# Check disk space
ansible all -i inventory -m command -a "df -h"

# Install a package (Debian/Ubuntu)
ansible all -i inventory -m apt -a "name=vim state=present"

# Install a package (RHEL/CentOS)
ansible all -i inventory -m yum -a "name=vim state=present"

# Copy a file
ansible all -i inventory -m copy -a "src=./file.txt dest=/tmp/file.txt"

# Create a directory
ansible all -i inventory -m file -a "path=/tmp/testdir state=directory"

# Manage a service
ansible all -i inventory -m service -a "name=nginx state=started enabled=yes"

# Restart a service on all servers
ansible production -i inventory -m service -a "name=nginx state=restarted"

Patterns

Ansible patterns let you target specific hosts:

# All hosts
ansible all

# Single host
ansible web1.example.com

# Group
ansible webservers

# Multiple groups (union)
ansible 'webservers:databases'

# Exclude
ansible '!databases'

# Intersection
ansible 'production:&webservers'

ansible.cfg Configuration

The ansible.cfg file configures Ansible behavior:

[defaults]
inventory = inventory
host_key_checking = False
retry_files_enabled = False
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts
fact_caching_timeout = 3600

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

[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
pipelining = True
Next: Playbooks Fundamentals β†’
← Back to Lessons

// Lesson 3: Playbooks Fundamentals

Playbooks are the heart of Ansible. They describe the desired state of your systems in a readable YAML format.

YAML Basics

Ansible playbooks are written in YAML. Key rules:

# Simple string
name: hello

# List
fruits:
  - apple
  - banana
  - cherry

# Dictionary
person:
  name: John
  age: 30

# Boolean (all these are equivalent)
enabled: true
enabled: yes
enabled: on

# Multi-line strings
description: |
  This is a multi-line
  string that preserves
  newlines.

Your First Playbook

# playbook.yml
---
- name: My first playbook
  hosts: webservers
  become: yes
  tasks:
    - name: Install nginx
      apt:
        name: nginx
        state: present
      when: ansible_os_family == "Debian"

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

Run it:

ansible-playbook -i inventory playbook.yml

Play Structure

Each play has several keys:

- name: Play description          # Optional, shown in output
  hosts: webservers              # Required - target hosts
  become: yes                   # Run as sudo
  become_user: root             # Become this user
  gather_facts: yes             # Gather system information
  vars:                          # Variables for this play
    nginx_port: 80
  vars_files:                   # Variable files
    - secrets.yml
  roles:                        # Roles to apply
    - common
    - nginx
  tasks:                        # Tasks to execute
    - name: Task description
      module_name:
        module_arg: value

Task Structure

tasks:
  - name: Install nginx
    apt:
      name: nginx
      state: present
      update_cache: yes
    register: apt_result        # Store result in variable
    changed_when: false        # Override changed status
    notify: restart nginx      # Trigger handler

  - name: Ensure nginx is running
    service:
      name: nginx
      state: started
    failed_when: false         # Don't fail if this errors

Handlers

Handlers are tasks that only run when notified:

tasks:
  - name: Copy nginx config
    copy:
      src: nginx.conf
      dest: /etc/nginx/nginx.conf
    notify: restart nginx

  - name: Enable nginx service
    service:
      name: nginx
      enabled: yes

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

Idempotency

Ansible modules are idempotentβ€”running them multiple times produces the same result:

# This is idempotent - running again won't change anything
- apt:
    name: nginx
    state: present

# This removes nginx - running again keeps it absent
- apt:
    name: nginx
    state: absent
Next: Modules Deep Dive β†’
← Back to Lessons

// Lesson 4: Modules Deep Dive

Modules are the building blocks of Ansible. There are thousands of modules for different tasks. Let's explore the most commonly used ones.

File Modules

file Module

# Create a directory
- file:
    path: /tmp/mydir
    state: directory
    mode: '0755'
    owner: root
    group: root

# Create a symlink
- file:
    src: /tmp/file
    dest: /tmp/link
    state: link

# Create an empty file
- file:
    path: /tmp/file
    state: touch

# Remove a file/directory
- file:
    path: /tmp/mydir
    state: absent

copy Module

# Copy a file
- copy:
    src: files/nginx.conf
    dest: /etc/nginx/nginx.conf
    owner: root
    group: root
    mode: '0644'
    validate: nginx -t %s  # Validate before copying

# Copy with content
- copy:
    dest: /tmp/hello.txt
    content: "Hello, World!\n"

Package Management

apt Module (Debian/Ubuntu)

# Install a package
- apt:
    name: nginx
    state: present
    update_cache: yes

# Install multiple packages
- apt:
    name:
      - nginx
      - vim
      - curl
    state: present

# Remove a package
- apt:
    name: nginx
    state: absent

# Upgrade all packages
- apt:
    upgrade: yes

yum/dnf Module (RHEL/CentOS)

# Install with yum
- yum:
    name: nginx
    state: present
    enablerepo: epel

# Install with dnf (RHEL 8+)
- dnf:
    name: nginx
    state: present

Service Module

# Start and enable a service
- service:
    name: nginx
    state: started
    enabled: yes

# Restart a service
- service:
    name: nginx
    state: restarted

# Stop a service
- service:
    name: nginx
    state: stopped

Command/Shell Modules

# Execute a command
- command: ls -la /tmp

# Execute a shell command (with shell features like pipes)
- shell: cat /etc/os-release | grep PRETTY_NAME

# Run only if a file doesn't exist
- command: /usr/bin/make_database.sh db_user
  args:
    creates: /etc/database_created  # Skip if this file exists

Best Practice: Use command/shell only when no module exists for your task. Modules are more idempotent and provide better error handling.

git Module

# Clone a repository
- git:
    repo: https://github.com/user/repo.git
    dest: /opt/repo
    version: main
    force: yes  # Update to latest even if exists

# Clone with depth (shallow clone)
- git:
    repo: https://github.com/user/repo.git
    dest: /opt/repo
    depth: 1

template Module

# Use a Jinja2 template
- template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    owner: root
    group: root
    mode: '0644'
    validate: nginx -t %s
    backup: yes

find Module

# Find files older than 30 days
- find:
    paths: /var/log
    age: 30d
    file_type: file
  register: old_files

# Delete old files
- file:
    path: "{{ item.path }}"
    state: absent
  loop: "{{ old_files.files }}"
Next: Variables & Facts β†’
← Back to Lessons

// Lesson 5: Variables & Facts

Variables and facts make your playbooks flexible and reusable. Facts are system information gathered by Ansible; variables are values you define.

Defining Variables

# In playbook
- hosts: webservers
  vars:
    nginx_port: 80
    app_path: /opt/myapp

# In inventory (group_vars)
# inventory
[webservers]
web1.example.com

# group_vars/webservers.yml
---
nginx_port: 80
app_env: production

# host_vars/web1.example.com.yml
---
app_path: /opt/web1

Using Variables

- name: Use variables
  hosts: webservers
  vars:
    port: 8080
  tasks:
    - name: Display variable
      debug:
        msg: "Port is {{ port }}"

    - name: Set a variable
      set_fact:
        full_url: "http://example.com:{{ port }}"

Facts

Facts are automatically gathered by Ansible (when gather_facts: yes):

- name: Show facts
  hosts: all
  tasks:
    - name: Display OS family
      debug:
        msg: "OS: {{ ansible_facts['os_family'] }}"

    - name: Display hostname
      debug:
        msg: "Hostname: {{ ansible_hostname }}"

    - name: Display IP addresses
      debug:
        msg: "IPs: {{ ansible_facts['all_ipv4_addresses'] }}"

    - name: Display memory
      debug:
        msg: "Memory: {{ (ansible_facts['memtotal_mb'] / 1024) | round(2) }} GB"

Common fact variables:

Registered Variables

- name: Register command output
  command: ls -la /tmp
  register: ls_output

- name: Display output
  debug:
    var: ls_output.stdout_lines

- name: Check if command changed
  debug:
    msg: "Command changed: {{ ls_output.changed }}"

Magic Variables

- name: Use magic variables
  hosts: webservers
  tasks:
    - name: Show inventory hostname
      debug:
        msg: "{{ inventory_hostname }}"

    - name: Show all hosts in group
      debug:
        msg: "{{ groups['webservers'] }}"

    - name: Hostvars of another host
      debug:
        msg: "{{ hostvars['db1.example.com']['ansible_facts']['ansible_distribution'] }}"

Variable Filters (Jinja2)

- name: Use filters
  hosts: all
  tasks:
    - debug:
        msg: "{{ nginx_port | default(80) }}"

    - debug:
        msg: "{{ 'hello' | upper }}"

    - debug:
        msg: "{{ ['a', 'b', 'c'] | join(',') }}"

    - debug:
        msg: "{{ 3.14159 | round(2) }}"

    - debug:
        msg: "{{ ansible_facts['memtotal_mb'] | human_readable }}"

    - debug:
        msg: "{{ {'a': 1, 'b': 2} | to_nice_json }}"
Next: Conditionals & Loops β†’
← Back to Lessons

// Lesson 6: Conditionals & Loops

Conditionals and loops let you create dynamic, flexible playbooks that respond to the state of your systems.

Conditionals (when)

- name: Install nginx on Debian
  apt:
    name: nginx
    state: present
  when: ansible_os_family == "Debian"

- name: Install httpd on RedHat
  yum:
    name: httpd
    state: present
  when: ansible_os_family == "RedHat"

# Multiple conditions
- name: Install Apache only if it's not already installed
  apt:
    name: apache2
    state: present
  when:
    - ansible_os_family == "Debian"
    - "'apache2' not in ansible_facts.packages"

Conditional Based on Registered Variable

- name: Check if file exists
  command: test -f /etc/special.conf
  register: special_file
  ignore_errors: yes

- name: Create file if missing
  file:
    path: /etc/special.conf
    state: touch
  when: special_file.rc != 0

Loops

Standard Loop (loop)

- name: Install multiple packages
  apt:
    name: "{{ item }}"
    state: present
  loop:
    - nginx
    - vim
    - curl
    - git

# With index
- name: Print with index
  debug:
    msg: "{{ index }}: {{ item }}"
  loop:
    - apple
    - banana
  loop_control:
    index_var: index

Loop with Dictionary

- name: Create users
  user:
    name: "{{ item.name }}"
    shell: "{{ item.shell }}"
    state: present
  loop:
    - { name: 'alice', shell: '/bin/bash' }
    - { name: 'bob', shell: '/bin/zsh' }
    - { name: 'charlie', shell: '/bin/false' }

iterate_hash

- name: Create users from dict
  user:
    name: "{{ item.key }}"
    shell: "{{ item.value.shell }}"
    state: present
  loop: "{{ {'alice': {'shell': '/bin/bash'}, 'bob': {'shell': '/bin/zsh'}}} | dict2items }}"

Loops with Conditions

- name: Install packages only on RedHat
  yum:
    name: "{{ item }}"
    state: present
  loop:
    - httpd
    - vim
    - curl
  when: ansible_os_family == "RedHat"

until (Retry)

- name: Wait for database to start
  wait_for:
    port: 5432
    host: localhost
    timeout: 30
  register: db_wait
  until: db_wait is succeeded
  delay: 2
  retries: 5

with_items vs loop

In modern Ansible, prefer loop over with_*:

# Old style (still works but deprecated)
- name: Old style
  apt:
    name: "{{ item }}"
  with_items:
    - nginx
    - vim

# New style (preferred)
- name: New style
  apt:
    name: "{{ item }}"
  loop:
    - nginx
    - vim
Next: Roles β†’
← Back to Lessons

// Lesson 7: Roles

Roles are the way to organize Ansible content into reusable components. They provide a structured way to package automation content.

Role Directory Structure

roles/
  nginx/
    β”œβ”€β”€ defaults/
    β”‚   └── main.yml         # Default variables (lowest priority)
    β”œβ”€β”€ files/               # Static files to copy
    β”œβ”€β”€ handlers/
    β”‚   └── main.yml         # Handlers
    β”œβ”€β”€ meta/                # Role metadata
    β”‚   └── main.yml
    β”œβ”€β”€ tasks/
    β”‚   └── main.yml        # Main tasks
    β”œβ”€β”€ templates/           # Jinja2 templates
    β”‚   └── nginx.conf.j2
    β”œβ”€β”€ tests/               # Test playbooks
    β”‚   β”œβ”€β”€ inventory
    β”‚   └── test.yml
    └── vars/
        └── main.yml         # Variables (highest priority)

Example Role: nginx

# roles/nginx/defaults/main.yml
---
nginx_port: 80
nginx_server_name: localhost
nginx_workers: 4

# roles/nginx/tasks/main.yml
---
- name: Install nginx
  apt:
    name: nginx
    state: present
  when: ansible_os_family == "Debian"

- name: Install nginx
  yum:
    name: nginx
    state: present
  when: ansible_os_family == "RedHat"

- name: Copy nginx config
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    validate: nginx -t %s
  notify: restart nginx

- name: Enable nginx
  service:
    name: nginx
    enabled: yes

# roles/nginx/handlers/main.yml
---
- name: restart nginx
  service:
    name: nginx
    state: restarted

# roles/nginx/templates/nginx.conf.j2
worker_processes {{ nginx_workers }};
events {
    worker_connections 1024;
}
http {
    server {
        listen {{ nginx_port }};
        server_name {{ nginx_server_name }};
    }
}

Using Roles in Playbook

# site.yml
---
- name: Configure webservers
  hosts: webservers
  become: yes
  roles:
    - nginx

# With role variables
- name: Configure webservers
  hosts: webservers
  become: yes
  roles:
    - role: nginx
      nginx_port: 8080

Role Dependencies

# roles/nginx/meta/main.yml
---
dependencies:
  - role: common
    tags: ['base']
  - role: logging
    when: enable_logging | default(true)

Ansible Galaxy

Ansible Galaxy is the community hub for sharing roles:

# Search for roles
ansible-galaxy search nginx

# Install a role
ansible-galaxy install nginx/nginx

# Create a role skeleton
ansible-galaxy init my_role

# List installed roles
ansible-galaxy list

Collections

Collections are the newer way to distribute Ansible content:

# Install collection
ansible-galaxy collection install community.general

# Use in playbook
- name: Use collection module
  community.general.homematic:
    host: 127.0.0.1
    vendor: homematic
Next: Templates β†’
← Back to Lessons

// Lesson 8: Templates

Ansible uses Jinja2 for templating. Templates let you create configuration files that adapt to different hosts and environments.

Basic Template

# templates/app.conf.j2
# Application configuration
app_name = {{ app_name }}
environment = {{ environment }}
port = {{ app_port }}

[database]
host = {{ db_host }}
port = {{ db_port }}
name = {{ db_name }}
# task
- template:
    src: app.conf.j2
    dest: /etc/myapp/app.conf
    mode: '0640'
    owner: root
    group: root

Template Conditionals

# templates/nginx.conf.j2
server {
{% if enable_ssl %}
    listen 443 ssl;
    ssl_certificate {{ ssl_cert_path }};
    ssl_certificate_key {{ ssl_key_path }};
{% else %}
    listen 80;
{% endif %}
    server_name {{ server_name }};

{% for port in proxy_ports %}
    location /{{ port }}/ {
        proxy_pass http://localhost:{{ port }}/;
    }
{% endfor %}
}

Template Filters

# Using filters in templates
# Convert to uppercase
{{ env | upper }}

# Default value
{{ mysql_port | default(3306) }}

# Join list
{{ domains | join(', ') }}

# First item
{{ users.0.name }}

# Check if defined
{{ nginx_path | default('/etc/nginx', true) }}

Template Tests

# Tests in templates
{% if ansible_facts['os_family'] == 'Debian' %}
# Debian-specific config
{% endif %}

{% if ssl_enabled is defined and ssl_enabled %}
# SSL config
{% endif %}

{% if 'webserver' in group_names %}
# Config for webservers
{% endif %}

Complex Example: HAProxy Config

# templates/haproxy.cfg.j2
global
    log /dev/log local0
    log /dev/log local1 notice
    maxconn {{ max_connections }}

defaults
    log     global
    mode    http
    option  httplog
    option  dontlognull
    timeout connect 5000
    timeout client  50000
    timeout server  50000

{% for backend in backends %}
backend {{ backend.name }}
    balance {{ backend.balance | default('roundrobin') }}
{% for server in backend.servers %}
    server {{ server.name }} {{ server.host }}:{{ server.port }} check
{% endfor %}
{% endfor %}
Next: Vault & Security β†’
← Back to Lessons

// Lesson 9: Vault & Security

Ansible Vault encrypts sensitive data within YAML files. Use it for passwords, API keys, certificates, and other secrets.

Creating Encrypted Files

# Create new encrypted file
ansible-vault create secrets.yml

# Encrypt existing file
ansible-vault encrypt secrets.yml

# View encrypted file
ansible-vault view secrets.yml

# Edit encrypted file
ansible-vault edit secrets.yml

# Decrypt file
ansible-vault decrypt secrets.yml

Using Vault Passwords

# Create password file
echo "my_vault_password" > .vault_pass

# Use with ansible-playbook
ansible-playbook site.yml --vault-password-file .vault_pass

# Or configure in ansible.cfg
# [defaults]
# vault_password_file = .vault_pass

Encrypted Variables

# secrets.yml (encrypted)
---
db_password: "supersecretpassword"
api_key: "1234567890abcdef"
ssl_certificate: |
  -----BEGIN CERTIFICATE-----
  ...
  -----END CERTIFICATE-----

# In playbook
- name: Use encrypted vars
  hosts: databases
  vars_files:
    - secrets.yml
  tasks:
    - name: Set database password
      mysql_user:
        name: appuser
        password: "{{ db_password }}"
        priv: '*.*:ALL'

Multiple Vaults

# Different passwords for different environments
ansible-vault create prod.yml --vault-password-file prod_vault_pass
ansible-vault create dev.yml --vault-password-file dev_vault_pass

# In playbook
- name: Production
  hosts: production
  vars_files:
    - prod.yml

- name: Development
  hosts: development
  vars_files:
    - dev.yml

Security Best Practices

# .gitignore
*.vault_pass
secrets.yml
prod_secrets.yml
Next: Error Handling β†’
← Back to Lessons

// Lesson 10: Error Handling

Ansible stops executing a playbook when a task fails. Error handling lets you control this behavior and recover from failures gracefully.

Ignoring Failures

- name: Try to stop service, but don't fail if it doesn't exist
  service:
    name: old_service
    state: stopped
  ignore_errors: yes

- name: Continue even if command fails
  command: /opt/scripts/optional_script.sh
  register: result
  failed_when: false

Blocks

- name: Block with rescue
  block:
    - name: Install and configure nginx
      apt:
        name: nginx
        state: present

    - name: Copy config
      copy:
        src: nginx.conf
        dest: /etc/nginx/nginx.conf

  rescue:
    - name: Rollback on failure
      apt:
        name: nginx
        state: absent
    - name: Notify about failure
      debug:
        msg: "Nginx installation failed!"

Always

- name: Always run cleanup
  block:
    - name: Deploy application
      git:
        repo: https://github.com/app/repo.git
        dest: /opt/app
  always:
    - name: Clean up temp files
      file:
        path: /tmp/app_build
        state: absent

Failed_when

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

- name: Conditional failure
  command: /usr/local/bin/custom_check.sh
  register: result
  failed_when:
    - result.rc != 0
    - "'critical' in result.stdout"

Debugging

- name: Debug output
  debug:
    msg: "Variable value is: {{ my_var }}"
    verbosity: 2  # Only show with -vvv

- name: Print all facts
  debug:
    var: ansible_facts
  when: ansible_facts is defined

# Using --start-at-task
ansible-playbook site.yml --start-at-task="Install nginx"

Check Mode (Dry Run)

# Run in check mode (no changes made)
ansible-playbook site.yml --check

# Check with diff
ansible-playbook site.yml --check --diff
Next: Testing & CI/CD β†’
← Back to Lessons

// Lesson 11: Testing & CI/CD

Testing your Ansible code ensures reliability and prevents production issues. Let's explore testing strategies and CI/CD integration.

ansible-lint

# Install
pip install ansible-lint

# Run linting
ansible-lint site.yml

# In CI/CD
- name: Lint Ansible
  uses: ansible/ansible-lint-action@v1

Molecule

Molecule tests roles against different instances:

# Install molecule
pip install molecule molecule-docker

# Initialize role with molecule
molecule init role --role-name my_role

# Run tests
cd roles/my_role
molecule test

# Test workflow
molecule create     # Create test instance
molecule converge   # Run playbook
molecule verify    # Run tests
molecule destroy   # Clean up
# molecule/default/converge.yml
---
- name: Converge
  hosts: all
  become: true
  roles:
    - role: my_role
      test_var: "test_value"

# molecule/default/verify.yml
---
- name: Verify
  hosts: all
  gather_facts: false
  tasks:
    - name: Check file exists
      stat:
        path: /etc/myapp/config
      register: config_file

    - name: Assert config file exists
      assert:
        that:
          - config_file.stat.exists

GitHub Actions Example

name: Ansible CI

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.11'
          
      - name: Install Ansible
        run: |
          pip install ansible ansible-lint molecule molecule-docker
          
      - name: Lint
        run: ansible-lint site.yml
        
      - name: Molecule Test
        run: molecule test

Test Kitchen (Alternative)

# .kitchen.yml
---
driver:
  name: docker

provisioner:
  name: ansible_playbook
  playbook: test/integration/default/test.yml

platforms:
  - name: ubuntu-20.04
  - name: centos-8

suites:
  - name: default
Next: Enterprise Patterns β†’
← Back to Lessons

// Lesson 12: Enterprise Patterns

For enterprise deployments, Ansible provides additional tools and patterns for scale, security, and collaboration.

Ansible Tower / AWX

Ansible Tower (Red Hat) and AWX (upstream) provide:

Collections

Collections package Playbooks, Roles, Modules, and Plugins:

# requirements.yml
collections:
  - name: community.general
  - name: ansible.posix
  - name: awx.awx
  - name: kubernetes.core

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

Execution Environments

Containerized Ansible environments for consistent execution:

# execution-environment.yml
---
version: 1

dependencies:
  galaxy: requirements.yml
  python: requirements.txt
  system: bindep.txt

build_arg: --container-engine docker

Directory Structure for Large Projects

infrastructure/
β”œβ”€β”€ ansible.cfg
β”œβ”€β”€ inventory/
β”‚   β”œβ”€β”€ production/
β”‚   β”‚   β”œβ”€β”€ hosts.yml
β”‚   β”‚   └── group_vars/
β”‚   └── development/
β”‚       β”œβ”€β”€ hosts.yml
β”‚       └── group_vars/
β”œβ”€β”€ playbooks/
β”‚   β”œβ”€β”€ site.yml
β”‚   β”œβ”€β”€ webservers.yml
β”‚   └── databases.yml
β”œβ”€β”€ roles/
β”‚   β”œβ”€β”€ common/
β”‚   β”œβ”€β”€ nginx/
β”‚   └── postgresql/
β”œβ”€β”€ collections/
β”‚   └── requirements.yml
└── library/

CI/CD with Automation Controller

The Road Ahead

Congratulations on completing this guide! You've learned:

Continue your journey with:

← Back to Lessons