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.
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