10.7 Ansible Core Concepts
Learning Objectives
By the end of this chapter, you will be able to:
Create and manage Ansible inventory files for different environments
Write structured Ansible playbooks using proper YAML syntax and conventions
Use Ansible modules effectively for common system administration tasks
Implement variables, facts, and templating for dynamic configurations
Apply conditionals and loops for flexible automation logic
Handle errors and implement proper task flow control
Debug playbooks and troubleshoot common execution issues
Organize playbooks and tasks for maintainability and reusability
Prerequisites: Ansible installed and basic understanding of YAML syntax. Access to Google Cloud Platform for hands-on examples.
Practical Focus: This chapter provides hands-on experience with Ansible fundamentals using real Google Cloud Platform infrastructure.
Ansible Inventory Management
Inventory is how Ansible knows which machines to manage and how to connect to them. It defines your infrastructure in a structured way that Ansible can understand and act upon.
Static Inventory Formats
INI Format (Traditional):
# inventory/production.ini
# Web servers in production
[web_servers]
web1.example.com ansible_host=34.123.45.67
web2.example.com ansible_host=34.123.45.68
web3.example.com ansible_host=34.123.45.69
# Database servers
[databases]
db1.example.com ansible_host=34.123.45.70 ansible_port=2222
db2.example.com ansible_host=34.123.45.71
# Load balancers
[load_balancers]
lb1.example.com ansible_host=34.123.45.72
# Group of groups
[production:children]
web_servers
databases
load_balancers
# Variables for all production hosts
[production:vars]
ansible_user=ubuntu
ansible_ssh_private_key_file=~/.ssh/gcp-key
environment=production
YAML Format (Modern):
# inventory/production.yml
all:
children:
production:
children:
web_servers:
hosts:
web1.example.com:
ansible_host: 34.123.45.67
web2.example.com:
ansible_host: 34.123.45.68
web3.example.com:
ansible_host: 34.123.45.69
vars:
nginx_port: 80
app_version: "1.2.3"
databases:
hosts:
db1.example.com:
ansible_host: 34.123.45.70
ansible_port: 2222
db2.example.com:
ansible_host: 34.123.45.71
vars:
mysql_port: 3306
backup_enabled: true
load_balancers:
hosts:
lb1.example.com:
ansible_host: 34.123.45.72
vars:
ansible_user: ubuntu
ansible_ssh_private_key_file: ~/.ssh/gcp-key
environment: production
region: us-central1
Dynamic Inventory for Google Cloud Platform
For cloud environments, dynamic inventory automatically discovers and manages your infrastructure:
# inventory/gcp.yml (dynamic inventory)
plugin: google.cloud.gcp_compute
projects:
- my-project-id
regions:
- us-central1
- us-east1
auth_kind: serviceaccount
service_account_file: ~/.gcp/service-account.json
# Group instances by labels
groups:
web_servers: "'web-server' in (labels.role | default(''))"
databases: "'database' in (labels.role | default(''))"
production: "'production' in (labels.env | default(''))"
staging: "'staging' in (labels.env | default(''))"
# Compose variables from instance metadata
compose:
ansible_host: networkInterfaces[0].accessConfigs[0].natIP | default(networkInterfaces[0].networkIP)
instance_type: machineType.split('/')[-1]
environment: labels.env | default('unknown')
Testing Inventory:
# List all hosts
ansible-inventory --list
# List hosts in specific group
ansible-inventory --list --group web_servers
# Test connectivity
ansible all -m ping
ansible web_servers -m ping -i inventory/production.yml
Playbook Structure and Syntax
Playbooks are Ansible’s configuration, deployment, and orchestration language. They describe the desired state of your systems in a human-readable YAML format.
Basic Playbook Structure:
---
# Basic playbook structure
- name: Configure web servers
hosts: web_servers
become: true
gather_facts: true
vars:
nginx_port: 80
app_name: "my-web-app"
tasks:
- name: Update package cache
apt:
update_cache: yes
cache_valid_time: 3600
- name: Install required packages
apt:
name:
- nginx
- git
- curl
- vim
state: present
- name: Start and enable Nginx
systemd:
name: nginx
state: started
enabled: yes
- name: Copy website files
copy:
src: ./website/
dest: /var/www/html/
owner: www-data
group: www-data
mode: '0644'
notify: restart nginx
handlers:
- name: restart nginx
systemd:
name: nginx
state: restarted
Playbook Components Explained:
Play Definition
- name: Configure web servers # Descriptive name hosts: web_servers # Target hosts/groups become: true # Use sudo/privilege escalation gather_facts: true # Collect system information
Variables Section
vars: nginx_port: 80 app_name: "my-web-app" packages: - nginx - git - curl
Tasks Section
tasks: - name: Install packages # Always provide descriptive names apt: # Module name name: "{{ packages }}" # Module parameters state: present tags: # Optional tags for selective execution - packages - installation
Handlers Section
handlers: - name: restart nginx # Triggered by notify directive systemd: name: nginx state: restarted
Multi-Play Playbooks:
---
# Configure database servers first
- name: Configure database servers
hosts: databases
become: true
tasks:
- name: Install MySQL
apt:
name: mysql-server
state: present
- name: Start MySQL service
systemd:
name: mysql
state: started
enabled: yes
# Then configure web servers
- name: Configure web servers
hosts: web_servers
become: true
tasks:
- name: Install Nginx
apt:
name: nginx
state: present
- name: Configure Nginx
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: restart nginx
handlers:
- name: restart nginx
systemd:
name: nginx
state: restarted
Essential Ansible Modules
Modules are the building blocks of Ansible automation. Each module performs a specific task and returns structured data about what it did.
System Modules:
# Package management
- name: Install packages (Ubuntu/Debian)
apt:
name: "{{ item }}"
state: present
update_cache: yes
loop:
- nginx
- git
- curl
# Service management
- name: Manage system services
systemd:
name: "{{ service_name }}"
state: started
enabled: yes
daemon_reload: yes
# User management
- name: Create application user
user:
name: appuser
system: yes
shell: /bin/bash
home: /var/www
create_home: yes
File and Directory Management:
# Create directories
- name: Create application directories
file:
path: "{{ item }}"
state: directory
owner: www-data
group: www-data
mode: '0755'
loop:
- /var/www/html
- /var/www/logs
- /var/www/config
# Copy files
- name: Copy static files
copy:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
owner: www-data
group: www-data
mode: "{{ item.mode }}"
loop:
- { src: "./files/index.html", dest: "/var/www/html/", mode: "0644" }
- { src: "./files/app.js", dest: "/var/www/html/", mode: "0644" }
# Use templates for dynamic content
- name: Generate configuration files
template:
src: nginx.conf.j2
dest: /etc/nginx/sites-available/default
backup: yes
notify: restart nginx
Command and Shell Modules:
# Run shell commands (when no specific module exists)
- name: Download and install custom software
shell: |
wget https://example.com/software.tar.gz -O /tmp/software.tar.gz
tar -xzf /tmp/software.tar.gz -C /opt/
/opt/software/install.sh
args:
creates: /opt/software/bin/app
warn: false
# Execute commands with specific environment
- name: Run application setup
command: /var/www/setup.py --initialize
environment:
PATH: "/var/www/venv/bin:{{ ansible_env.PATH }}"
DATABASE_URL: "{{ database_connection_string }}"
become_user: appuser
register: setup_result
# Check command output
- name: Display setup results
debug:
var: setup_result.stdout_lines
when: setup_result.changed
Google Cloud Platform Modules:
# Manage GCP compute instances
- name: Create GCP compute instance
google.cloud.gcp_compute_instance:
name: web-server-01
machine_type: e2-standard-2
zone: us-central1-a
project: "{{ gcp_project }}"
auth_kind: serviceaccount
service_account_file: "{{ gcp_service_account_file }}"
disks:
- auto_delete: true
boot: true
initialize_params:
source_image: projects/ubuntu-os-cloud/global/images/family/ubuntu-2204-lts
disk_size_gb: 20
network_interfaces:
- network: "{{ gcp_network }}"
access_configs:
- name: External NAT
type: ONE_TO_ONE_NAT
tags:
items:
- web-server
- production
state: present
# Manage firewall rules
- name: Allow HTTP traffic
google.cloud.gcp_compute_firewall:
name: allow-http
network: "{{ gcp_network }}"
project: "{{ gcp_project }}"
auth_kind: serviceaccount
service_account_file: "{{ gcp_service_account_file }}"
allowed:
- ip_protocol: tcp
ports:
- '80'
- '443'
source_ranges:
- 0.0.0.0/0
target_tags:
- web-server
state: present
Variables and Templating
Variables make your playbooks flexible and reusable across different environments and configurations.
Variable Precedence (highest to lowest):
Command line values (ansible-playbook -e “var=value”)
Role vars
Inventory file or script group vars
Inventory group_vars/*
Playbook group_vars/*
Inventory file or script host vars
Inventory host_vars/*
Playbook host_vars/*
Host facts / cached facts
Play vars
Play vars_prompt
Play vars_files
Role defaults
Inventory file or script default vars
Variable Definition Examples:
# In playbook
- name: Deploy application
hosts: web_servers
vars:
app_name: "my-web-app"
app_version: "1.2.3"
app_port: 8080
# Complex variables
database_config:
host: "{{ hostvars['db1.example.com']['ansible_default_ipv4']['address'] }}"
port: 3306
name: "{{ app_name }}_production"
# List variables
required_packages:
- nginx
- python3
- python3-pip
- git
Variable Files:
# group_vars/web_servers.yml
nginx_worker_processes: 4
nginx_worker_connections: 1024
nginx_keepalive_timeout: 65
ssl_certificate_email: admin@example.com
ssl_certificate_domains:
- example.com
- www.example.com
# Environment-specific variables
database_url: "postgresql://user:pass@db1.example.com:5432/myapp"
redis_url: "redis://cache1.example.com:6379/0"
secret_key: "{{ vault_secret_key }}"
Jinja2 Templates:
Templates allow you to create dynamic configuration files:
{# templates/nginx.conf.j2 #}
user www-data;
worker_processes {{ nginx_worker_processes }};
pid /run/nginx.pid;
events {
worker_connections {{ nginx_worker_connections }};
use epoll;
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
# Gzip compression
gzip on;
gzip_vary on;
gzip_types text/plain text/css application/json application/javascript;
# Virtual hosts
{% for domain in ssl_certificate_domains %}
server {
listen 80;
server_name {{ domain }};
location / {
proxy_pass http://127.0.0.1:{{ app_port }};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
{% endfor %}
}
Using Templates in Playbooks:
- name: Generate Nginx configuration
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
backup: yes
notify: restart nginx
- name: Generate application configuration
template:
src: app_config.py.j2
dest: /var/www/{{ app_name }}/config.py
owner: www-data
group: www-data
mode: '0600'
notify: restart application
Conditionals and Loops
Conditionals and loops make your playbooks dynamic and able to handle different scenarios and data structures.
Conditionals:
# Simple conditionals
- name: Install Apache (RedHat family)
yum:
name: httpd
state: present
when: ansible_os_family == "RedHat"
- name: Install Apache (Debian family)
apt:
name: apache2
state: present
when: ansible_os_family == "Debian"
# Complex conditionals
- name: Configure SSL
template:
src: ssl.conf.j2
dest: /etc/nginx/ssl.conf
when:
- ssl_enabled | default(false)
- ssl_certificate_path is defined
- environment == "production"
# Conditional based on command output
- name: Check if application is installed
command: which myapp
register: app_check
ignore_errors: yes
changed_when: false
- name: Install application
shell: curl -sSL https://install.myapp.com | bash
when: app_check.rc != 0
Loops:
# Simple loops
- name: Install multiple packages
apt:
name: "{{ item }}"
state: present
loop:
- nginx
- git
- curl
- vim
# Loop with dictionaries
- name: Create multiple users
user:
name: "{{ item.name }}"
groups: "{{ item.groups }}"
shell: "{{ item.shell }}"
state: present
loop:
- { name: "alice", groups: "sudo,developers", shell: "/bin/bash" }
- { name: "bob", groups: "developers", shell: "/bin/zsh" }
- { name: "charlie", groups: "operators", shell: "/bin/bash" }
# Loop with conditionals
- name: Install development tools
apt:
name: "{{ item }}"
state: present
loop:
- nodejs
- npm
- python3-dev
- build-essential
when:
- install_dev_tools | default(false)
- ansible_distribution == "Ubuntu"
# Loop with complex data structures
- name: Configure virtual hosts
template:
src: vhost.conf.j2
dest: "/etc/nginx/sites-available/{{ item.name }}"
loop: "{{ virtual_hosts }}"
notify: restart nginx
when: item.enabled | default(true)
Advanced Loop Control:
# Loop with index
- name: Create numbered backup directories
file:
path: "/backup/daily-{{ ansible_loop.index }}"
state: directory
loop: "{{ range(1, 8) | list }}"
# Loop until condition is met
- name: Wait for service to be ready
uri:
url: "http://{{ inventory_hostname }}:8080/health"
method: GET
register: health_check
until: health_check.status == 200
retries: 10
delay: 5
# Loop with variable registration
- name: Check disk usage on multiple mounts
shell: "df -h {{ item }}"
register: disk_usage
loop:
- /
- /var
- /tmp
- name: Display disk usage results
debug:
msg: "{{ item.stdout }}"
loop: "{{ disk_usage.results }}"
Error Handling and Task Control
Proper error handling ensures your playbooks are robust and provide meaningful feedback when things go wrong.
Basic Error Handling:
# Ignore errors for specific tasks
- name: Try to install optional package
apt:
name: some-optional-package
state: present
ignore_errors: yes
# Register task results for checking
- name: Download application archive
get_url:
url: "https://releases.example.com/app-{{ app_version }}.tar.gz"
dest: "/tmp/app-{{ app_version }}.tar.gz"
register: download_result
- name: Fail if download was unsuccessful
fail:
msg: "Failed to download application archive"
when: download_result is failed
# Continue on failure but register the error
- name: Attempt service restart
systemd:
name: "{{ service_name }}"
state: restarted
register: restart_result
failed_when: false
- name: Handle restart failure
debug:
msg: "Service restart failed: {{ restart_result.msg }}"
when: restart_result is failed
Block and Rescue:
# Handle errors with blocks
- name: Deploy application with error handling
block:
- name: Stop application service
systemd:
name: myapp
state: stopped
- name: Backup current version
command: "cp -r /var/www/myapp /var/www/myapp.backup.{{ ansible_date_time.epoch }}"
- name: Deploy new version
unarchive:
src: "/tmp/app-{{ app_version }}.tar.gz"
dest: /var/www/myapp
remote_src: yes
- name: Start application service
systemd:
name: myapp
state: started
rescue:
- name: Restore previous version on failure
command: "cp -r /var/www/myapp.backup.{{ ansible_date_time.epoch }} /var/www/myapp"
- name: Restart application with old version
systemd:
name: myapp
state: started
- name: Notify about deployment failure
mail:
to: ops-team@example.com
subject: "Deployment failed on {{ inventory_hostname }}"
body: "Application deployment failed and was rolled back"
always:
- name: Clean up temporary files
file:
path: "/tmp/app-{{ app_version }}.tar.gz"
state: absent
Custom Failure Conditions:
# Define custom failure conditions
- name: Check application health
uri:
url: "http://{{ inventory_hostname }}:8080/health"
method: GET
register: health_response
failed_when:
- health_response.status != 200
- "'healthy' not in health_response.json.status"
# Define when task should be considered changed
- name: Update configuration file
lineinfile:
path: /etc/myapp/config.ini
line: "environment={{ environment }}"
regexp: "^environment="
register: config_update
changed_when: "'line replaced' in config_update.msg"
Debugging and Development
Effective debugging techniques help you develop and troubleshoot playbooks efficiently.
Debug Module:
# Display variable values
- name: Debug variables
debug:
msg: |
App Name: {{ app_name }}
App Version: {{ app_version }}
Environment: {{ environment }}
Database URL: {{ database_url }}
# Display complex data structures
- name: Show all network interfaces
debug:
var: ansible_interfaces
- name: Show specific interface details
debug:
var: ansible_default_ipv4
# Conditional debugging
- name: Debug only in development
debug:
msg: "Development mode enabled - extra logging active"
when: environment == "development"
Verbosity Control:
# Run playbook with different verbosity levels
ansible-playbook site.yml # Standard output
ansible-playbook site.yml -v # Verbose
ansible-playbook site.yml -vv # More verbose
ansible-playbook site.yml -vvv # Debug
ansible-playbook site.yml -vvvv # Connection debug
Check Mode (Dry Run):
# Preview changes without making them
ansible-playbook site.yml --check
# Show differences for template and file operations
ansible-playbook site.yml --check --diff
# Run specific tasks in check mode
ansible-playbook site.yml --check --tags "configuration"
Step Mode:
# Execute playbook step by step
ansible-playbook site.yml --step
Limiting Execution:
# Run only on specific hosts
ansible-playbook site.yml --limit "web_servers"
ansible-playbook site.yml --limit "web1.example.com,web2.example.com"
# Skip specific hosts
ansible-playbook site.yml --limit "all:!databases"
# Run specific tags
ansible-playbook site.yml --tags "installation,configuration"
ansible-playbook site.yml --skip-tags "database"
Note
The concepts in this chapter form the foundation for all Ansible automation. Practice with simple examples before moving to more complex scenarios in the following chapters.