yaml Configuration Management September 1, 2025

Ansible Role Skeleton

A complete Ansible role structure with tasks, handlers, templates, and Molecule testing. Use this as a starting point for any new role.

ansiblerolesautomationtemplate

Description

A production-ready Ansible role skeleton following best practices. Includes proper task organization, handlers, templates, variables with validation, and Molecule testing setup.

Role Structure

# Generate role skeleton
ansible-galaxy role init my_role --offline

# Or create manually:
mkdir -p roles/my_role/{tasks,handlers,templates,files,vars,defaults,meta,molecule/default}
roles/my_role/
├── defaults/
│   └── main.yml          # Default variables (lowest precedence)
├── vars/
│   └── main.yml          # Role variables
├── tasks/
│   ├── main.yml          # Main entry point
│   ├── install.yml       # Installation tasks
│   ├── configure.yml     # Configuration tasks
│   └── service.yml       # Service management
├── handlers/
│   └── main.yml          # Handler definitions
├── templates/
│   └── config.j2         # Jinja2 templates
├── files/
│   └── script.sh         # Static files
├── meta/
│   └── main.yml          # Role metadata
└── molecule/
    └── default/
        ├── molecule.yml
        ├── converge.yml
        └── verify.yml

defaults/main.yml

---
# ════════════════════════════════════════════════════════════
# My Role - Default Variables
# ════════════════════════════════════════════════════════════

# Package settings
my_role_package_name: myapp
my_role_package_state: present
my_role_version: "1.0.0"

# Service settings
my_role_service_name: myapp
my_role_service_enabled: true
my_role_service_state: started

# Configuration
my_role_config_dir: /etc/myapp
my_role_data_dir: /var/lib/myapp
my_role_log_dir: /var/log/myapp

my_role_user: myapp
my_role_group: myapp

# Application settings
my_role_port: 8080
my_role_bind_address: "0.0.0.0"
my_role_workers: "{{ ansible_processor_vcpus }}"
my_role_max_connections: 1000

# Feature flags
my_role_enable_ssl: false
my_role_enable_metrics: true
my_role_enable_debug: false

# SSL settings (when enabled)
my_role_ssl_cert: ""
my_role_ssl_key: ""

# Logging
my_role_log_level: info
my_role_log_format: json

# Resource limits
my_role_memory_limit: "512M"
my_role_cpu_limit: "1.0"

vars/main.yml

---
# ════════════════════════════════════════════════════════════
# Role Variables (higher precedence than defaults)
# ════════════════════════════════════════════════════════════

# OS-specific package names are loaded from vars/{{ ansible_os_family }}.yml
my_role_supported_os:
  - Debian
  - RedHat

my_role_required_packages:
  - curl
  - ca-certificates

tasks/main.yml

---
# ════════════════════════════════════════════════════════════
# My Role - Main Tasks
# ════════════════════════════════════════════════════════════

- name: Validate OS family
  ansible.builtin.assert:
    that:
      - ansible_os_family in my_role_supported_os
    fail_msg: "OS family {{ ansible_os_family }} is not supported"
    success_msg: "OS family {{ ansible_os_family }} is supported"

- name: Include OS-specific variables
  ansible.builtin.include_vars: "{{ item }}"
  with_first_found:
    - "{{ ansible_distribution }}-{{ ansible_distribution_version }}.yml"
    - "{{ ansible_distribution }}.yml"
    - "{{ ansible_os_family }}.yml"
    - default.yml
  tags: always

- name: Install packages
  ansible.builtin.import_tasks: install.yml
  tags:
    - my_role
    - my_role:install

- name: Configure application
  ansible.builtin.import_tasks: configure.yml
  tags:
    - my_role
    - my_role:configure

- name: Manage service
  ansible.builtin.import_tasks: service.yml
  tags:
    - my_role
    - my_role:service

tasks/install.yml

---
# ════════════════════════════════════════════════════════════
# Installation Tasks
# ════════════════════════════════════════════════════════════

- name: Install required packages
  ansible.builtin.package:
    name: "{{ my_role_required_packages }}"
    state: present

- name: Create application user
  ansible.builtin.user:
    name: "{{ my_role_user }}"
    group: "{{ my_role_group }}"
    system: true
    shell: /usr/sbin/nologin
    home: "{{ my_role_data_dir }}"
    create_home: false

- name: Create application directories
  ansible.builtin.file:
    path: "{{ item }}"
    state: directory
    owner: "{{ my_role_user }}"
    group: "{{ my_role_group }}"
    mode: '0755'
  loop:
    - "{{ my_role_config_dir }}"
    - "{{ my_role_data_dir }}"
    - "{{ my_role_log_dir }}"

- name: Install application package
  ansible.builtin.package:
    name: "{{ my_role_package_name }}"
    state: "{{ my_role_package_state }}"
  notify: Restart myapp

tasks/configure.yml

---
# ════════════════════════════════════════════════════════════
# Configuration Tasks
# ════════════════════════════════════════════════════════════

- name: Deploy main configuration
  ansible.builtin.template:
    src: config.j2
    dest: "{{ my_role_config_dir }}/config.yml"
    owner: "{{ my_role_user }}"
    group: "{{ my_role_group }}"
    mode: '0640'
    validate: myapp validate --config %s
  notify: Reload myapp

- name: Deploy systemd service file
  ansible.builtin.template:
    src: myapp.service.j2
    dest: /etc/systemd/system/{{ my_role_service_name }}.service
    owner: root
    group: root
    mode: '0644'
  notify:
    - Reload systemd
    - Restart myapp

tasks/service.yml

---
# ════════════════════════════════════════════════════════════
# Service Management Tasks
# ════════════════════════════════════════════════════════════

- name: Ensure service is in desired state
  ansible.builtin.service:
    name: "{{ my_role_service_name }}"
    state: "{{ my_role_service_state }}"
    enabled: "{{ my_role_service_enabled }}"

- name: Wait for service to be ready
  ansible.builtin.wait_for:
    port: "{{ my_role_port }}"
    host: "{{ my_role_bind_address | default('127.0.0.1') }}"
    delay: 5
    timeout: 60
  when: my_role_service_state == 'started'

handlers/main.yml

---
# ════════════════════════════════════════════════════════════
# Handlers
# ════════════════════════════════════════════════════════════

- name: Reload systemd
  ansible.builtin.systemd:
    daemon_reload: true
  listen: Reload systemd

- name: Restart myapp
  ansible.builtin.service:
    name: "{{ my_role_service_name }}"
    state: restarted
  listen: Restart myapp

- name: Reload myapp
  ansible.builtin.service:
    name: "{{ my_role_service_name }}"
    state: reloaded
  listen: Reload myapp

templates/config.j2

# {{ ansible_managed }}
# My Application Configuration

server:
  bind: {{ my_role_bind_address }}
  port: {{ my_role_port }}
  workers: {{ my_role_workers }}
  max_connections: {{ my_role_max_connections }}

{% if my_role_enable_ssl %}
ssl:
  enabled: true
  certificate: {{ my_role_ssl_cert }}
  key: {{ my_role_ssl_key }}
{% endif %}

logging:
  level: {{ my_role_log_level }}
  format: {{ my_role_log_format }}
  path: {{ my_role_log_dir }}/myapp.log

{% if my_role_enable_metrics %}
metrics:
  enabled: true
  path: /metrics
  port: {{ my_role_port | int + 1 }}
{% endif %}

debug: {{ my_role_enable_debug | lower }}

molecule/default/molecule.yml

---
dependency:
  name: galaxy

driver:
  name: docker

platforms:
  - name: ubuntu-22
    image: ubuntu:22.04
    pre_build_image: true
    command: /lib/systemd/systemd
    privileged: true
    volumes:
      - /sys/fs/cgroup:/sys/fs/cgroup:rw
  - name: debian-12
    image: debian:12
    pre_build_image: true

provisioner:
  name: ansible
  inventory:
    host_vars:
      ubuntu-22:
        ansible_python_interpreter: /usr/bin/python3

verifier:
  name: ansible

molecule/default/verify.yml

---
- name: Verify
  hosts: all
  gather_facts: false
  tasks:
    - name: Check service is running
      ansible.builtin.service:
        name: myapp
        state: started
      check_mode: true
      register: service_status
      failed_when: service_status.changed

    - name: Check port is listening
      ansible.builtin.wait_for:
        port: 8080
        timeout: 10

    - name: Check config file exists
      ansible.builtin.stat:
        path: /etc/myapp/config.yml
      register: config_file
      failed_when: not config_file.stat.exists

Usage

# Run molecule tests
cd roles/my_role
molecule test

# Just run converge (apply role)
molecule converge

# Run verify only
molecule verify

# Login to test container
molecule login -h ubuntu-22