Ansible and YAML
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)
- Extra vars (-e in command line)
- Task vars (only for the task)
- Block vars (only for tasks in block)
- Role and include vars
- Play vars_files
- Play vars_prompt
- Play vars
- Set_facts / registered vars
- Host facts
- Playbook host_vars/*
- Playbook group_vars/*
- Inventory host_vars/*
- Inventory group_vars/*
- Inventory vars
- 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