Ansible Automation for RHEL/CentOS: Infrastructure as Code

Tyler Maginnis | February 24, 2024

AnsibleautomationCentOSRHELIaCconfiguration managementDevOps

Need Professional CentOS/RHEL Support?

Get expert assistance with your centos/rhel support implementation and management. Tyler on Tech Louisville provides priority support for Louisville businesses.

Same-day service available for Louisville area

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.