Ansible Automation for RHEL/CentOS: Infrastructure as Code
Ansible provides agentless automation for CentOS/RHEL environments, enabling infrastructure as code, configuration management, and application deployment. This guide covers advanced Ansible techniques for enterprise automation.
Ansible Fundamentals
Installation and Setup
Install and configure Ansible:
# Install Ansible on control node
yum install epel-release
yum install ansible ansible-doc
# Install from pip for latest version
pip3 install ansible ansible-lint
# Verify installation
ansible --version
ansible-playbook --version
# Configure Ansible
cat > /etc/ansible/ansible.cfg <<EOF
[defaults]
inventory = /etc/ansible/inventory
remote_user = ansible
host_key_checking = False
retry_files_enabled = False
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts
fact_caching_timeout = 86400
stdout_callback = yaml
callback_whitelist = profile_tasks, timer
deprecation_warnings = False
[privilege_escalation]
become = True
become_method = sudo
become_user = root
become_ask_pass = False
[ssh_connection]
ssh_args = -C -o ControlMaster=auto -o ControlPersist=60s
pipelining = True
control_path = /tmp/ansible-%%h-%%p-%%r
EOF
# Create ansible user on all nodes
useradd -m -s /bin/bash ansible
echo "ansible ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/ansible
SSH Key Management
Configure passwordless authentication:
# Generate SSH key for ansible user
su - ansible
ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa -N ""
# Distribute SSH keys
for host in node1 node2 node3; do
ssh-copy-id ansible@$host
done
# Test connectivity
ansible all -m ping
# Use SSH agent
eval $(ssh-agent)
ssh-add ~/.ssh/id_rsa
Inventory Management
Static Inventory
Create inventory files:
# /etc/ansible/inventory/hosts.yml
all:
children:
webservers:
hosts:
web1:
ansible_host: 192.168.1.10
ansible_port: 22
web2:
ansible_host: 192.168.1.11
http_port: 8080
vars:
ansible_user: ansible
ansible_python_interpreter: /usr/bin/python3
databases:
hosts:
db1:
ansible_host: 192.168.1.20
db_port: 3306
db2:
ansible_host: 192.168.1.21
db_port: 5432
vars:
ansible_become: yes
ansible_become_method: sudo
loadbalancers:
hosts:
lb1:
ansible_host: 192.168.1.30
vars:
haproxy_stats_port: 8404
vars:
ansible_connection: ssh
ntp_server: time.example.com
dns_servers:
- 8.8.8.8
- 8.8.4.4
Dynamic Inventory
Create dynamic inventory scripts:
#!/usr/bin/env python3
# dynamic_inventory.py
import json
import argparse
import subprocess
def get_inventory():
inventory = {
"_meta": {
"hostvars": {}
}
}
# Get VM list from cloud provider
# This is a simplified example
vms = [
{"name": "web-prod-1", "ip": "10.0.1.10", "role": "webserver"},
{"name": "web-prod-2", "ip": "10.0.1.11", "role": "webserver"},
{"name": "db-prod-1", "ip": "10.0.2.10", "role": "database"},
]
for vm in vms:
# Add to role group
if vm["role"] not in inventory:
inventory[vm["role"]] = {"hosts": [], "vars": {}}
inventory[vm["role"]]["hosts"].append(vm["name"])
# Add host variables
inventory["_meta"]["hostvars"][vm["name"]] = {
"ansible_host": vm["ip"],
"ansible_user": "ansible"
}
return inventory
def get_host(hostname):
# Return variables for a specific host
inventory = get_inventory()
return inventory["_meta"]["hostvars"].get(hostname, {})
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--list", action="store_true")
parser.add_argument("--host", type=str)
args = parser.parse_args()
if args.list:
print(json.dumps(get_inventory(), indent=2))
elif args.host:
print(json.dumps(get_host(args.host), indent=2))
Playbook Development
Basic Playbook Structure
Create well-structured playbooks:
---
# site.yml - Main playbook
- name: Configure web servers
hosts: webservers
become: yes
gather_facts: yes
vars:
packages_to_install:
- nginx
- php-fpm
- git
pre_tasks:
- name: Update package cache
yum:
update_cache: yes
changed_when: false
tasks:
- name: Install required packages
yum:
name: "{{ packages_to_install }}"
state: present
notify: restart services
- name: Configure firewall
firewalld:
service: "{{ item }}"
permanent: yes
state: enabled
immediate: yes
loop:
- http
- https
- name: Deploy configuration files
template:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
owner: root
group: root
mode: '0644'
loop:
- { src: nginx.conf.j2, dest: /etc/nginx/nginx.conf }
- { src: php-fpm.conf.j2, dest: /etc/php-fpm.d/www.conf }
notify: restart services
handlers:
- name: restart services
systemd:
name: "{{ item }}"
state: restarted
enabled: yes
loop:
- nginx
- php-fpm
post_tasks:
- name: Verify services are running
systemd:
name: "{{ item }}"
state: started
loop:
- nginx
- php-fpm
Advanced Playbook Features
Implement complex automation:
---
# advanced-playbook.yml
- name: Advanced deployment playbook
hosts: all
gather_facts: yes
vars_files:
- vars/common.yml
- "vars/{{ ansible_distribution }}.yml"
tasks:
- name: Include OS-specific tasks
include_tasks: "tasks/{{ ansible_distribution }}.yml"
- name: Configure system with blocks
block:
- name: Configure networking
template:
src: network.j2
dest: /etc/sysconfig/network-scripts/ifcfg-eth0
- name: Configure DNS
lineinfile:
path: /etc/resolv.conf
line: "nameserver {{ item }}"
loop: "{{ dns_servers }}"
rescue:
- name: Rollback network configuration
command: cp /etc/sysconfig/network-scripts/ifcfg-eth0.backup /etc/sysconfig/network-scripts/ifcfg-eth0
- name: Send alert
mail:
to: admin@example.com
subject: "Network configuration failed on {{ inventory_hostname }}"
body: "Please check the system"
always:
- name: Log configuration attempt
lineinfile:
path: /var/log/ansible-deployments.log
line: "{{ ansible_date_time.iso8601 }} - Network configuration attempted"
create: yes
- name: Conditional task execution
package:
name: httpd
state: present
when:
- ansible_os_family == "RedHat"
- "'webservers' in group_names"
- ansible_distribution_major_version | int >= 7
- name: Loop with conditions
user:
name: "{{ item.name }}"
groups: "{{ item.groups }}"
state: present
loop:
- { name: developer1, groups: [developers, docker] }
- { name: developer2, groups: [developers] }
- { name: admin1, groups: [wheel, docker] }
when: item.name in authorized_users
- name: Using register and debug
command: df -h /
register: disk_usage
changed_when: false
- name: Check disk space
fail:
msg: "Insufficient disk space: {{ disk_usage.stdout_lines[1].split()[4] }}"
when: disk_usage.stdout_lines[1].split()[4] | regex_replace('%', '') | int > 80
Ansible Roles
Creating Roles
Build reusable roles:
# Create role structure
cd /etc/ansible/roles
ansible-galaxy init webserver
# Role directory structure
tree webserver/
# webserver/
# ├── defaults/
# │ └── main.yml
# ├── files/
# ├── handlers/
# │ └── main.yml
# ├── meta/
# │ └── main.yml
# ├── README.md
# ├── tasks/
# │ └── main.yml
# ├── templates/
# ├── tests/
# │ ├── inventory
# │ └── test.yml
# └── vars/
# └── main.yml
Role Implementation
Create a comprehensive role:
# roles/webserver/tasks/main.yml
---
- name: Install web server packages
yum:
name: "{{ webserver_packages }}"
state: present
tags:
- packages
- webserver
- name: Create web root directory
file:
path: "{{ webserver_document_root }}"
state: directory
owner: "{{ webserver_user }}"
group: "{{ webserver_group }}"
mode: '0755'
- name: Deploy nginx configuration
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
backup: yes
notify: restart nginx
tags:
- configuration
- name: Configure SELinux for web server
seboolean:
name: "{{ item }}"
state: yes
persistent: yes
loop:
- httpd_can_network_connect
- httpd_can_network_connect_db
when: ansible_selinux.status == "enabled"
- name: Configure firewall rules
firewalld:
service: "{{ item }}"
permanent: yes
state: enabled
immediate: yes
loop:
- http
- https
tags:
- firewall
# roles/webserver/handlers/main.yml
---
- name: restart nginx
systemd:
name: nginx
state: restarted
daemon_reload: yes
- name: reload nginx
systemd:
name: nginx
state: reloaded
# roles/webserver/defaults/main.yml
---
webserver_packages:
- nginx
- nginx-mod-stream
- nginx-mod-http-perl
webserver_user: nginx
webserver_group: nginx
webserver_document_root: /var/www/html
webserver_port: 80
webserver_ssl_port: 443
# roles/webserver/meta/main.yml
---
galaxy_info:
author: your_name
description: Nginx web server role for CentOS/RHEL
company: your_company
license: MIT
min_ansible_version: 2.9
platforms:
- name: EL
versions:
- 7
- 8
galaxy_tags:
- web
- nginx
- centos
- rhel
dependencies:
- role: common
vars:
common_packages:
- vim
- htop
Templates and Variables
Jinja2 Templates
Create dynamic configurations:
{# templates/nginx.conf.j2 #}
# Nginx configuration - Managed by Ansible
# {{ ansible_managed }}
user {{ webserver_user }};
worker_processes {{ ansible_processor_vcpus }};
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;
include /usr/share/nginx/modules/*.conf;
events {
worker_connections {{ nginx_worker_connections | default(1024) }};
}
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Load balancing configuration
upstream backend {
{% for host in groups['appservers'] %}
server {{ hostvars[host]['ansible_default_ipv4']['address'] }}:{{ app_port | default(8080) }}{% if hostvars[host]['weight'] is defined %} weight={{ hostvars[host]['weight'] }}{% endif %};
{% endfor %}
{% if enable_backup_server | default(false) %}
server {{ backup_server }}:{{ app_port | default(8080) }} backup;
{% endif %}
}
server {
listen {{ webserver_port }} default_server;
listen [::]:{{ webserver_port }} default_server;
server_name {{ ansible_fqdn }} {{ server_aliases | join(' ') }};
root {{ webserver_document_root }};
{% if enable_ssl | default(false) %}
listen {{ webserver_ssl_port }} ssl http2 default_server;
listen [::]:{{ webserver_ssl_port }} ssl http2 default_server;
ssl_certificate {{ ssl_certificate_path }};
ssl_certificate_key {{ ssl_certificate_key_path }};
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 10m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
{% endif %}
location / {
proxy_pass http://backend;
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;
}
{% for location in custom_locations | default([]) %}
location {{ location.path }} {
{{ location.config | indent(12) }}
}
{% endfor %}
}
}
Variable Precedence
Manage variables effectively:
# group_vars/all.yml
---
company_name: "Example Corp"
ntp_servers:
- 0.centos.pool.ntp.org
- 1.centos.pool.ntp.org
default_packages:
- vim-enhanced
- wget
- curl
- net-tools
# group_vars/webservers.yml
---
nginx_worker_connections: 2048
enable_ssl: true
ssl_certificate_path: /etc/ssl/certs/server.crt
ssl_certificate_key_path: /etc/ssl/private/server.key
# host_vars/web1.yml
---
server_aliases:
- www.example.com
- example.com
weight: 2
Ansible Vault
Securing Sensitive Data
Encrypt sensitive information:
# Create encrypted file
ansible-vault create vars/secrets.yml
# Encrypt existing file
ansible-vault encrypt vars/passwords.yml
# Edit encrypted file
ansible-vault edit vars/secrets.yml
# View encrypted file
ansible-vault view vars/secrets.yml
# Decrypt file
ansible-vault decrypt vars/secrets.yml
# Rekey vault file
ansible-vault rekey vars/secrets.yml
Using Vault in Playbooks
Implement vault in automation:
# vars/secrets.yml (encrypted)
---
database_password: "SuperSecret123!"
api_key: "abcd1234efgh5678"
ssl_private_key: |
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA...
-----END RSA PRIVATE KEY-----
# playbook-with-vault.yml
---
- name: Deploy application with secrets
hosts: appservers
vars_files:
- vars/common.yml
- vars/secrets.yml
tasks:
- name: Deploy database configuration
template:
src: database.conf.j2
dest: /etc/myapp/database.conf
owner: myapp
group: myapp
mode: '0600'
no_log: true # Prevent logging sensitive data
# Running playbook with vault
# ansible-playbook playbook-with-vault.yml --ask-vault-pass
# or
# ansible-playbook playbook-with-vault.yml --vault-password-file ~/.vault_pass
Error Handling and Debugging
Error Handling Strategies
Implement robust error handling:
---
- name: Deployment with error handling
hosts: all
any_errors_fatal: true # Stop on first error
max_fail_percentage: 30 # Allow 30% failure
tasks:
- name: Critical task with retry
command: /usr/local/bin/critical-operation
register: result
until: result.rc == 0
retries: 3
delay: 10
ignore_errors: no
- name: Task with custom failure condition
shell: df -h / | tail -1 | awk '{print $5}' | sed 's/%//'
register: disk_usage
failed_when: disk_usage.stdout | int > 90
changed_when: false
- name: Task with error handling block
block:
- name: Try to perform operation
command: /usr/local/bin/risky-operation
register: operation_result
- name: Verify operation success
command: /usr/local/bin/verify-operation
when: operation_result.rc == 0
rescue:
- name: Rollback on failure
command: /usr/local/bin/rollback-operation
- name: Send notification
uri:
url: "https://alerts.example.com/webhook"
method: POST
body_format: json
body:
text: "Operation failed on {{ inventory_hostname }}"
severity: "critical"
always:
- name: Cleanup temporary files
file:
path: /tmp/operation-temp
state: absent
Debugging Techniques
Debug Ansible playbooks:
# debug-playbook.yml
---
- name: Debugging playbook
hosts: localhost
gather_facts: yes
tasks:
- name: Show all variables
debug:
var: hostvars[inventory_hostname]
tags: debug
- name: Show specific facts
debug:
msg: |
Hostname: {{ ansible_hostname }}
OS: {{ ansible_distribution }} {{ ansible_distribution_version }}
Memory: {{ ansible_memtotal_mb }} MB
CPU cores: {{ ansible_processor_vcpus }}
- name: Debug with verbosity
debug:
msg: "This only shows with -vv"
verbosity: 2
- name: Pause for inspection
pause:
prompt: "Check the system and press Enter to continue"
when: debug_mode | default(false)
# Run with debugging
# ansible-playbook debug-playbook.yml -vvv
# ansible-playbook debug-playbook.yml --step
# ansible-playbook debug-playbook.yml --check --diff
Advanced Automation
Custom Modules
Create custom Ansible modules:
#!/usr/bin/python
# library/check_service_port.py
from ansible.module_utils.basic import AnsibleModule
import socket
import time
def check_port(host, port, timeout=5):
"""Check if a port is open on a host"""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
try:
result = sock.connect_ex((host, port))
sock.close()
return result == 0
except:
return False
def main():
module = AnsibleModule(
argument_spec=dict(
host=dict(type='str', required=True),
port=dict(type='int', required=True),
timeout=dict(type='int', default=5),
retries=dict(type='int', default=3),
delay=dict(type='int', default=1)
)
)
host = module.params['host']
port = module.params['port']
timeout = module.params['timeout']
retries = module.params['retries']
delay = module.params['delay']
for attempt in range(retries):
if check_port(host, port, timeout):
module.exit_json(
changed=False,
msg=f"Port {port} is open on {host}",
attempts=attempt + 1
)
time.sleep(delay)
module.fail_json(
msg=f"Port {port} is not open on {host} after {retries} attempts"
)
if __name__ == '__main__':
main()
Dynamic Includes and Imports
Use dynamic task inclusion:
# main-playbook.yml
---
- name: Dynamic configuration
hosts: all
tasks:
- name: Gather service facts
service_facts:
- name: Include tasks based on running services
include_tasks: "configure-{{ item.key }}.yml"
loop: "{{ ansible_facts.services | dict2items }}"
when:
- item.value.state == "running"
- item.key in ['nginx', 'httpd', 'mysql']
- name: Import role conditionally
import_role:
name: "{{ item }}"
loop:
- database
- webserver
- monitoring
when: item in required_roles
CI/CD Integration
Ansible in Pipelines
Integrate with CI/CD systems:
# .gitlab-ci.yml
stages:
- validate
- test
- deploy
variables:
ANSIBLE_FORCE_COLOR: "true"
ANSIBLE_HOST_KEY_CHECKING: "false"
validate:
stage: validate
script:
- ansible-playbook --syntax-check site.yml
- ansible-lint site.yml
test:
stage: test
script:
- ansible-playbook -i inventory/test site.yml --check --diff
deploy_staging:
stage: deploy
script:
- ansible-playbook -i inventory/staging site.yml
only:
- develop
deploy_production:
stage: deploy
script:
- ansible-playbook -i inventory/production site.yml --vault-password-file $VAULT_PASS
only:
- master
when: manual
Testing with Molecule
Test Ansible roles:
# molecule/default/molecule.yml
---
dependency:
name: galaxy
driver:
name: docker
platforms:
- name: centos8
image: centos:8
pre_build_image: true
command: /sbin/init
privileged: true
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:ro
provisioner:
name: ansible
playbooks:
converge: converge.yml
verify: verify.yml
inventory:
host_vars:
centos8:
ansible_python_interpreter: /usr/bin/python3
verifier:
name: ansible
# molecule/default/converge.yml
---
- name: Converge
hosts: all
roles:
- role: webserver
# Run tests
# molecule test
# molecule converge
# molecule verify
Performance Optimization
Ansible Performance Tuning
Optimize playbook execution:
# ansible.cfg optimizations
[defaults]
strategy = free
forks = 50
poll_interval = 15
host_key_checking = False
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/facts_cache
fact_caching_timeout = 86400
[ssh_connection]
ssh_args = -C -o ControlMaster=auto -o ControlPersist=60s -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no
control_path = /tmp/ansible-ssh-%%h-%%p-%%r
pipelining = True
# Optimize playbooks
---
- name: Optimized playbook
hosts: all
gather_facts: no # Disable if not needed
pre_tasks:
- name: Gather only required facts
setup:
gather_subset:
- network
- virtual
when: ansible_facts is not defined
tasks:
- name: Use batch operations
yum:
name:
- nginx
- php-fpm
- mariadb
state: present
- name: Use async for long-running tasks
command: /usr/local/bin/long-running-task
async: 3600
poll: 0
register: long_task
- name: Check async task
async_status:
jid: "{{ long_task.ansible_job_id }}"
register: job_result
until: job_result.finished
retries: 60
delay: 60
Best Practices
Ansible Project Structure
Organize Ansible projects:
ansible-project/
├── ansible.cfg
├── inventory/
│ ├── production/
│ │ ├── group_vars/
│ │ ├── host_vars/
│ │ └── hosts.yml
│ └── staging/
│ ├── group_vars/
│ ├── host_vars/
│ └── hosts.yml
├── library/
├── module_utils/
├── filter_plugins/
├── roles/
│ ├── requirements.yml
│ ├── common/
│ ├── webserver/
│ └── database/
├── playbooks/
│ ├── site.yml
│ ├── webservers.yml
│ └── databases.yml
└── docs/
└── README.md
Security Best Practices
# Security considerations
---
- name: Secure playbook
hosts: all
become: yes
become_method: sudo
vars:
ansible_ssh_common_args: '-o StrictHostKeyChecking=yes'
ansible_become_pass: !vault |
$ANSIBLE_VAULT;1.1;AES256
66383439383437363535393139323434
tasks:
- name: Ensure secure permissions
file:
path: "{{ item.path }}"
owner: "{{ item.owner }}"
group: "{{ item.group }}"
mode: "{{ item.mode }}"
loop:
- { path: /etc/ssh/sshd_config, owner: root, group: root, mode: '0600' }
- { path: /etc/sudoers, owner: root, group: root, mode: '0440' }
no_log: true # Don't log sensitive operations
Conclusion
Ansible provides powerful automation capabilities for CentOS/RHEL environments. Master playbooks, roles, and advanced features to implement infrastructure as code, ensuring consistent, repeatable, and scalable system configurations. Regular testing and adherence to best practices ensure reliable automation in production environments.