Ansible and YAML

Ansible is a powerful automation tool that uses YAML (YAML Ain't Markup Language) for configuration management, application deployment, and task automation. Understanding both Ansible and YAML is essential for modern DevOps and infrastructure management.

1. YAML Fundamentals

What is YAML?

YAML is a human-readable data serialization standard that's commonly used for configuration files and data exchange between applications. It's designed to be easily readable by both humans and machines.

YAML Syntax Rules

  • Indentation: Uses spaces (not tabs) for structure
  • Case Sensitive: Keys and values are case-sensitive
  • Key-Value Pairs: Separated by colon and space
  • Lists: Items start with dash and space
  • Comments: Start with # symbol

YAML Data Types

  • Scalars: Strings, numbers, booleans
  • Sequences: Lists or arrays
  • Mappings: Key-value pairs (dictionaries)
  • Multi-line Strings: Using | or > operators

YAML Examples

# Simple key-value pairs
name: John Doe
age: 30
active: true

# Lists
fruits:
  - apple
  - banana
  - orange

# Nested structures
person:
  name: Jane Smith
  address:
    street: 123 Main St
    city: New York
    zip: 10001

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

summary: >
  This is a folded
  string that will be
  converted to a single line.

2. Ansible Overview

What is Ansible?

Ansible is an open-source automation platform that enables infrastructure as code, configuration management, application deployment, and orchestration. It's agentless and uses SSH for communication with managed nodes.

Key Features

  • Agentless: No software installation required on managed nodes
  • Idempotent: Safe to run multiple times
  • Simple: Uses YAML for easy readability
  • Powerful: Extensive module library
  • Flexible: Works with various platforms and cloud providers

Ansible Components

  • Control Node: Machine where Ansible is installed
  • Managed Nodes: Target systems being managed
  • Inventory: List of managed nodes
  • Modules: Units of work executed by Ansible
  • Tasks: Individual actions performed by modules
  • Playbooks: YAML files containing tasks
  • Roles: Reusable collections of tasks

3. Ansible Inventory

Inventory Formats

  • INI Format: Traditional configuration file format
  • YAML Format: More structured and readable
  • Dynamic Inventory: Scripts that generate inventory

INI Inventory Example

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

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

[production:children]
webservers
databases

[webservers:vars]
http_port=80
maxRequestsPerChild=808

YAML Inventory Example

all:
  children:
    webservers:
      hosts:
        web1.example.com:
        web2.example.com:
      vars:
        http_port: 80
        maxRequestsPerChild: 808
    databases:
      hosts:
        db1.example.com:
        db2.example.com:
    production:
      children:
        webservers:
        databases:

4. Ansible Playbooks

Playbook Structure

  • Play: Maps hosts to tasks
  • Tasks: List of actions to perform
  • Handlers: Tasks triggered by notifications
  • Variables: Data used in tasks
  • Templates: Files with variable substitution

Basic Playbook Example

---
- name: Configure web servers
  hosts: webservers
  become: yes
  vars:
    http_port: 80
    max_clients: 200
  
  tasks:
    - name: Install Apache
      package:
        name: httpd
        state: present
    
    - name: Start Apache service
      service:
        name: httpd
        state: started
        enabled: yes
      notify: restart apache
    
    - name: Copy configuration file
      template:
        src: httpd.conf.j2
        dest: /etc/httpd/conf/httpd.conf
      notify: restart apache
  
  handlers:
    - name: restart apache
      service:
        name: httpd
        state: restarted

5. Ansible Modules

Common Module Categories

  • System Modules: user, group, cron, service
  • Package Modules: package, yum, apt, pip
  • File Modules: copy, template, file, lineinfile
  • Network Modules: uri, get_url, firewalld
  • Cloud Modules: ec2, azure, gcp
  • Database Modules: mysql_user, postgresql_db

Module Examples

# Package management
- name: Install packages
  package:
    name:
      - nginx
      - git
      - vim
    state: present

# File operations
- name: Create directory
  file:
    path: /opt/myapp
    state: directory
    mode: '0755'
    owner: www-data
    group: www-data

# Copy files
- name: Copy application files
  copy:
    src: app/
    dest: /opt/myapp/
    owner: www-data
    group: www-data
    mode: '0644'

# Template processing
- name: Generate config file
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    backup: yes
  notify: reload nginx

# Command execution
- name: Run custom script
  command: /opt/myapp/deploy.sh
  args:
    chdir: /opt/myapp
    creates: /opt/myapp/deployed.flag

6. Variables and Facts

Variable Types

  • Playbook Variables: Defined in playbooks
  • Inventory Variables: Host and group variables
  • Role Variables: Defined in roles
  • Extra Variables: Passed via command line
  • Facts: Automatically gathered system information

Variable Precedence (highest to lowest)

  1. Extra vars (-e in command line)
  2. Task vars (only for the task)
  3. Block vars (only for tasks in block)
  4. Role and include vars
  5. Play vars_files
  6. Play vars_prompt
  7. Play vars
  8. Set_facts / registered vars
  9. Host facts
  10. Playbook host_vars/*
  11. Playbook group_vars/*
  12. Inventory host_vars/*
  13. Inventory group_vars/*
  14. Inventory vars
  15. Role defaults

Using Variables

---
- name: Variable examples
  hosts: all
  vars:
    app_name: myapp
    app_version: 1.0.0
    users:
      - name: alice
        uid: 1001
      - name: bob
        uid: 1002
  
  tasks:
    - name: Display variable
      debug:
        msg: "Installing {{ app_name }} version {{ app_version }}"
    
    - name: Create users
      user:
        name: "{{ item.name }}"
        uid: "{{ item.uid }}"
        state: present
      loop: "{{ users }}"
    
    - name: Show system facts
      debug:
        msg: "OS: {{ ansible_distribution }} {{ ansible_distribution_version }}"
    
    - name: Conditional task
      package:
        name: apache2
        state: present
      when: ansible_os_family == "Debian"

7. Conditionals and Loops

Conditional Statements

# Simple condition
- name: Install package on Ubuntu
  apt:
    name: nginx
    state: present
  when: ansible_distribution == "Ubuntu"

# Multiple conditions
- name: Install on specific versions
  package:
    name: docker
    state: present
  when:
    - ansible_distribution == "CentOS"
    - ansible_distribution_major_version == "7"

# Complex conditions
- name: Complex condition
  service:
    name: httpd
    state: started
  when: (ansible_distribution == "CentOS" and ansible_distribution_major_version == "7") or
        (ansible_distribution == "Ubuntu" and ansible_distribution_version == "18.04")

Loops

# Simple loop
- name: Install packages
  package:
    name: "{{ item }}"
    state: present
  loop:
    - git
    - vim
    - curl

# Loop with dictionaries
- name: Create users
  user:
    name: "{{ item.name }}"
    uid: "{{ item.uid }}"
    groups: "{{ item.groups }}"
  loop:
    - { name: alice, uid: 1001, groups: "wheel,users" }
    - { name: bob, uid: 1002, groups: "users" }

# Loop with conditions
- name: Install packages conditionally
  package:
    name: "{{ item }}"
    state: present
  loop:
    - nginx
    - apache2
    - lighttpd
  when: item != "apache2" or ansible_distribution != "CentOS"

8. Ansible Roles

Role Structure

roles/
  common/
    tasks/
      main.yml
    handlers/
      main.yml
    templates/
      ntp.conf.j2
    files/
      bar.txt
    vars/
      main.yml
    defaults/
      main.yml
    meta/
      main.yml
    README.md

Role Example

# roles/webserver/tasks/main.yml
---
- name: Install web server
  package:
    name: "{{ web_server_package }}"
    state: present

- name: Copy configuration
  template:
    src: "{{ web_server_config_template }}"
    dest: "{{ web_server_config_path }}"
  notify: restart web server

- name: Start web server
  service:
    name: "{{ web_server_service }}"
    state: started
    enabled: yes

# roles/webserver/defaults/main.yml
---
web_server_package: nginx
web_server_service: nginx
web_server_config_template: nginx.conf.j2
web_server_config_path: /etc/nginx/nginx.conf

# roles/webserver/handlers/main.yml
---
- name: restart web server
  service:
    name: "{{ web_server_service }}"
    state: restarted

# Using the role in a playbook
---
- name: Configure web servers
  hosts: webservers
  become: yes
  roles:
    - common
    - webserver

9. Templates with Jinja2

Template Syntax

  • Variables: {{ variable_name }}
  • Control Structures: {% if %}, {% for %}, {% endif %}
  • Comments: {# comment #}
  • Filters: {{ variable | filter }}

Template Example

# templates/nginx.conf.j2
user {{ nginx_user }};
worker_processes {{ ansible_processor_vcpus }};

events {
    worker_connections {{ nginx_worker_connections }};
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    
    {% if nginx_gzip_enabled %}
    gzip on;
    gzip_types text/plain text/css application/json;
    {% endif %}
    
    {% for server in nginx_servers %}
    server {
        listen {{ server.port }};
        server_name {{ server.name }};
        
        location / {
            root {{ server.document_root }};
            index index.html index.htm;
        }
        
        {% if server.ssl_enabled %}
        listen 443 ssl;
        ssl_certificate {{ server.ssl_cert }};
        ssl_certificate_key {{ server.ssl_key }};
        {% endif %}
    }
    {% endfor %}
}

10. Error Handling

Error Handling Strategies

# Ignore errors
- name: Command that might fail
  command: /bin/false
  ignore_errors: yes

# Handle failures
- name: Attempt risky operation
  command: risky_command
  register: result
  failed_when: result.rc != 0 and "expected error" not in result.stderr

# Rescue blocks
- name: Handle errors with blocks
  block:
    - name: Risky task
      command: might_fail
  rescue:
    - name: Handle failure
      debug:
        msg: "Task failed, running recovery"
    - name: Recovery action
      command: recovery_command
  always:
    - name: Always run this
      debug:
        msg: "This always runs"

# Custom failure conditions
- name: Check service status
  command: systemctl is-active nginx
  register: nginx_status
  failed_when: nginx_status.rc != 0
  changed_when: false

11. Best Practices

Playbook Organization

  • Use Roles: Organize tasks into reusable roles
  • Directory Structure: Follow standard Ansible directory layout
  • Naming Conventions: Use descriptive names for tasks and variables
  • Documentation: Comment complex logic and document variables

Security Best Practices

  • Vault: Encrypt sensitive data with ansible-vault
  • Least Privilege: Use become only when necessary
  • SSH Keys: Use SSH key authentication
  • No Passwords: Avoid hardcoding passwords

Performance Optimization

  • Parallelism: Use forks and serial settings
  • Fact Caching: Cache facts to reduce gathering time
  • Pipelining: Enable SSH pipelining
  • Strategy: Use appropriate execution strategies

12. Ansible Vault

Encrypting Data

# Encrypt a file
ansible-vault encrypt secrets.yml

# Create encrypted file
ansible-vault create secrets.yml

# Edit encrypted file
ansible-vault edit secrets.yml

# Decrypt file
ansible-vault decrypt secrets.yml

# View encrypted file
ansible-vault view secrets.yml

# Encrypt string
ansible-vault encrypt_string 'secret_password' --name 'db_password'

Using Encrypted Variables

# In playbook
---
- name: Deploy application
  hosts: webservers
  vars_files:
    - secrets.yml
  tasks:
    - name: Configure database
      template:
        src: database.conf.j2
        dest: /etc/app/database.conf
      vars:
        db_password: "{{ vault_db_password }}"

# Running with vault
ansible-playbook -i inventory playbook.yml --ask-vault-pass
ansible-playbook -i inventory playbook.yml --vault-password-file vault_pass.txt

13. Testing and Debugging

Testing Strategies

  • Syntax Check: ansible-playbook --syntax-check
  • Dry Run: ansible-playbook --check
  • Diff Mode: ansible-playbook --diff
  • Molecule: Testing framework for Ansible roles
  • Ansible Lint: Static analysis tool

Debugging Techniques

# Debug module
- name: Show variable value
  debug:
    var: my_variable

- name: Show custom message
  debug:
    msg: "The value is {{ my_variable }}"

# Verbose output
ansible-playbook -vvv playbook.yml

# Step through tasks
ansible-playbook --step playbook.yml

# Start at specific task
ansible-playbook --start-at-task="Install packages" playbook.yml

14. Advanced Topics

Custom Modules

# Simple custom module (library/hello.py)
#!/usr/bin/python
from ansible.module_utils.basic import AnsibleModule

def main():
    module = AnsibleModule(
        argument_spec=dict(
            name=dict(type='str', required=True)
        )
    )
    
    name = module.params['name']
    result = dict(
        changed=False,
        message=f"Hello, {name}!"
    )
    
    module.exit_json(**result)

if __name__ == '__main__':
    main()

# Using custom module
- name: Use custom module
  hello:
    name: "World"
  register: result

- name: Show result
  debug:
    msg: "{{ result.message }}"

Dynamic Inventory

# Dynamic inventory script (inventory.py)
#!/usr/bin/env python3
import json
import sys

def get_inventory():
    inventory = {
        'webservers': {
            'hosts': ['web1.example.com', 'web2.example.com'],
            'vars': {
                'http_port': 80
            }
        },
        '_meta': {
            'hostvars': {
                'web1.example.com': {
                    'ansible_host': '192.168.1.10'
                },
                'web2.example.com': {
                    'ansible_host': '192.168.1.11'
                }
            }
        }
    }
    return inventory

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

15. Integration with CI/CD

GitLab CI Example

# .gitlab-ci.yml
stages:
  - test
  - deploy

test_ansible:
  stage: test
  script:
    - ansible-lint playbooks/
    - ansible-playbook --syntax-check playbooks/site.yml
    - molecule test

deploy_staging:
  stage: deploy
  script:
    - ansible-playbook -i inventory/staging playbooks/site.yml
  environment:
    name: staging
  only:
    - develop

deploy_production:
  stage: deploy
  script:
    - ansible-playbook -i inventory/production playbooks/site.yml
  environment:
    name: production
  only:
    - master
  when: manual

16. Conclusion

Ansible and YAML provide a powerful combination for infrastructure automation and configuration management. By understanding YAML syntax and Ansible concepts, you can create maintainable, scalable automation solutions that improve operational efficiency and reduce manual errors.

Key takeaways:

  • YAML's human-readable format makes configuration management accessible
  • Ansible's agentless architecture simplifies deployment and management
  • Idempotency ensures safe and predictable automation
  • Roles and templates promote code reusability
  • Proper testing and security practices are essential