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:

  1. 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
    
  2. Variables Section

    vars:
      nginx_port: 80
      app_name: "my-web-app"
      packages:
        - nginx
        - git
        - curl
    
  3. 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
    
  4. 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):

  1. Command line values (ansible-playbook -e “var=value”)

  2. Role vars

  3. Inventory file or script group vars

  4. Inventory group_vars/*

  5. Playbook group_vars/*

  6. Inventory file or script host vars

  7. Inventory host_vars/*

  8. Playbook host_vars/*

  9. Host facts / cached facts

  10. Play vars

  11. Play vars_prompt

  12. Play vars_files

  13. Role defaults

  14. 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.