A production-grade, cross-distribution Ansible framework implementing the Unified Loader System architecture. Supports Alpine, Debian/Ubuntu, and RHEL/CentOS with consistent APIs across native packages and container runtimes.
- Universal Compatibility: Single codebase for Alpine, Debian, and RHEL families
- Dual Delivery Modes: Seamless switching between native packages and containers
- Loader Architecture: Modular, reusable components with standardized interfaces
- Enterprise-Grade: Full audit logging, idempotency, and safety guarantees
- Self-Documenting: Every loader includes Simple + Comprehensive usage examples
- Quick Start
- Architecture
- Node Tags
- Development: Development
- Best Practices: Best Practices
- Contributing: Contributing
- Python Requirements:
- Control Node: Python 3.10+ (for modern dev-tooling support).
- Managed Nodes: Python 3.7+ (Physical minimum).
- Note: For older systems (e.g., OpenSUSE Leap 15.x with Python 3.6), the system automatically side-loads Python 3.11 during initialization.
# Install Ansible and required collections
pip install ansible
ansible-galaxy collection install -r requirements.yml# Initialize new servers
ansible-playbook -i inventory/dev.yml playbooks/base/init.yml
# With vault for sensitive data
ansible-playbook -i inventory/prod.yml playbooks/base/init.yml \
-e "@~/.uniloader/.vault.yml" --vault-password-file ~/.uniloader/.vault_pass# Container mode
- ansible.builtin.include_role:
name: "app"
vars:
app_name: "nginx"
app_delivery_mode: "container"
app_image: "nginx:alpine"
app_ports: ["80:80"]
# Native mode
- ansible.builtin.include_role:
name: "app"
vars:
app_name: "nginx"
app_delivery_mode: "native"
app_packages: ["nginx"]
app_state: "started"The project implements a four-layer architecture:
┌─────────────────────────────────────────────────────────┐
│ Layer 4: Orchestration (Playbooks) │
│ - Scenario composition │
│ - Multi-role coordination │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Layer 3: Application API (app role) │
│ - Unified deployment interface │
│ - Mode selection (container vs native) │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Layer 2: Implementation (container, native roles) │
│ - Container: Docker, Podman, Containerd, CRI-O │
│ - Native: Package, Service, File loaders │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Layer 1: Foundation (facts_loader, os_loader) │
│ - OS detection and normalization │
│ - Privilege management (become_enabled) │
│ - Cross-distribution abstraction │
└─────────────────────────────────────────────────────────┘
The system employs a deterministic resolution logic for both variables and tasks:
Variables are merged following a "General to Specific" funnel to ensure environmental constraints override technical defaults:
default.yml: Absolute base defaults.{{ app_name }}.yml: Global application metadata (version, origin).{{ mode }}.yml: Delivery mode technical baseline (e.g., native/container defaults).{{ distro }}.yml: Environmental Constraint Layer (e.g., Alpine-specific package names).{{ mode }}/default.yml: Mode-specific technical baseline.{{ mode }}/{{ distro }}.yml: Precision Override Layer (e.g., specific native config for Alpine).
Tasks are resolved using a "Specific to General" search to provide the most optimized implementation:
mode/distro-version.yml: Precision specialized implementation.mode/distro.yml: OS-specific implementation for the delivery mode.mode/app_name.yml: Application-specific logic within a delivery mode.app_name.yml: Universal application logic.mode/default.yml->mode.yml: Fallback to generic delivery mode logic.distro.yml->default.yml: Final fallback to generic OS/base tasks.
| Loader | Purpose | Example |
|---|---|---|
facts_loader |
OS detection, fingerprinting | Auto-detects Alpine/Debian/RHEL |
os_loader |
Distribution-specific dispatch | Routes to alpine.yml/debian.yml/redhat.yml |
package_loader |
Universal package management | Works with apk/apt/dnf |
service_loader |
Service lifecycle control | Handles systemd/openrc/sysvinit |
container_loader |
Container orchestration | Supports Docker/Podman/Containerd |
docker_compose_loader |
Multi-container apps | Docker Compose V2 integration |
The package_loader supports 38+ installation methods via unified prefix syntax:
| Category | Prefix | Examples |
|---|---|---|
| System Package Managers | (none) | nginx, curl, vim |
| Local Files | (path) | /tmp/app.deb, /tmp/app.rpm, /tmp/app.apk |
| Remote URLs | (https) | https://example.com/app.deb |
| macOS Formats | (path) | .dmg, .pkg, .mpkg, .app, .zip, .framework, .dylib, .kext |
| Mac App Store | mas: |
mas:497799835, mas:Xcode |
| Windows Winget | winget: |
winget:Microsoft.VisualStudioCode |
| Chocolatey | choco: |
choco:7zip, choco:nodejs-lts |
| Scoop | scoop: |
scoop:neovim, scoop:fzf |
| MS Store | msstore: |
msstore:9NBLGGH5R558 |
| Snap | snap: |
snap:code, snap:spotify |
| Flatpak | flatpak: |
flatpak:org.gimp.GIMP |
| Nix | nix: |
nix:ripgrep, nix:fd |
| GNU Guix | guix: |
guix:emacs |
| AUR | aur: |
aur:google-chrome |
| Python pip | pip: |
pip:ansible-lint, pip:requests==2.28.0 |
| Python pipx | pipx: |
pipx:black, pipx:poetry |
| Conda | conda: |
conda:numpy, conda:pandas |
| npm | npm: |
npm:typescript, npm:eslint |
| Yarn | yarn: |
yarn:prettier |
| pnpm | pnpm: |
pnpm:vite |
| Bun | bun: |
bun:elysia |
| Deno | deno: |
deno:fresh |
| Cargo | cargo: |
cargo:ripgrep, cargo:bat |
| Go | go: |
go:github.com/junegunn/fzf@latest |
| Ruby gem | gem: |
gem:rails, gem:bundler |
| Composer | composer: |
composer:laravel/installer |
| Dart | dart: |
dart:stagehand |
| Nim | nim: |
nim:choosenim |
| .NET | dotnet: |
dotnet:dotnet-ef |
| Coursier | cs: |
cs:scalafmt |
| SDKMAN | sdk: |
sdk:java, sdk:gradle |
See roles/native/tasks/package_loader/main.yml for complete API reference
Node tags enable dynamic behavior based on host characteristics:
host- Physical or virtual machinecontainer- Container environmentdocker_host- Docker daemon hostpodman_host- Podman runtime hostdocker_container- Running inside Dockerpodman_container- Running inside Podman
open- Unrestricted internet accessrestricted- Behind firewall/proxy (uses mirrors)
dev- Development environmenttest- Testing environmentstage- Staging environmentprod- Production environment
all:
hosts:
dev-web:
node_tags: ["host", "docker_host", "dev", "open"]
prod-db:
node_tags: ["host", "podman_host", "prod", "restricted"]
test-container:
node_tags: ["container", "docker_container", "test"]# Start development containers
docker compose -f docker/docker-compose.yml up -d
# Access test environments
docker exec -it alpine /bin/sh
docker exec -it debian /bin/bash
docker exec -it redhat /bin/bash
# Run playbooks against local containers
ansible-playbook -i inventory/dev.yml playbooks/base/init.yml# Verbose output levels
ansible-playbook -v # Basic task output
ansible-playbook -vv # Task input/output
ansible-playbook -vvv # Connection debugging
ansible-playbook -vvvv # Full SSH debugging
# Limit to specific hosts
ansible-playbook -i inventory/dev.yml playbooks/base/init.yml --limit alpine
# Check mode (dry run)
ansible-playbook -i inventory/dev.yml playbooks/base/init.yml --checkEvery new loader MUST include:
# =====================================================================
# roles/[role]/tasks/[loader_name].yml
#
# Purpose:
# [Clear description of what this loader does]
#
# Simple Usage:
# - ansible.builtin.include_role:
# name: "[role]"
# tasks_from: "[loader_name]"
# vars:
# [minimal_required_param]: "value"
#
# Comprehensive Usage:
# - ansible.builtin.include_role:
# name: "[role]"
# tasks_from: "[loader_name]"
# vars:
# [param1]: "value1"
# [param2]: "value2"
# # ... ALL available parameters with realistic values
# become_enabled: true
#
# =====================================================================- Simple Usage: Minimal parameters for quick adoption
- Comprehensive Usage: Complete API reference with ALL parameters
Example: See roles/container/tasks/docker_compose_loader.yml
❌ Wrong:
- debug:
msg: "Hello"✅ Correct:
- ansible.builtin.debug:
msg: "Hello"All tasks with names MUST:
- Use double quotes
- Append
os_fingerprintsuffix
- name: "Install packages {{ '(' ~ os_fingerprint ~ ')' if os_fingerprint is defined else '' }}"
ansible.builtin.package:
name: "{{ packages }}"Exception: assert, include_role, include_tasks, import_* MUST NOT have names.
❌ Wrong:
when: my_var
when: my_var == true✅ Correct:
when: my_var is defined and my_var | bool
when: my_var | default(false, true) | boolAll shell and command tasks MUST define:
- name: "Execute script {{ '(' ~ os_fingerprint ~ ')' if os_fingerprint is defined else '' }}"
ansible.builtin.shell: |
set -eu
./my-script.sh
changed_when: "'Updated' in script_result.stdout"
failed_when: script_result.rc != 0
register: script_resultUse the standardized become_enabled variable:
- name: "Install system package {{ '(' ~ os_fingerprint ~ ')' if os_fingerprint is defined else '' }}"
ansible.builtin.package:
name: "nginx"
state: "present"
become: "{{ become_enabled }}"This automatically handles:
- Container environments (no become needed)
- Root user connections (no become needed)
- Regular user connections (become enabled)
Important: All tasks requiring privilege escalation MUST use become: "{{ become_enabled }}" instead of hardcoded become: true, except for tasks in roles/bootstrap/ where become: true is required for initial system setup.
❌ Never use delegate_to or run_once on include directives:
# WRONG - Will cause parser errors
- name: "Load configuration"
ansible.builtin.include_role:
name: "native"
tasks_from: "log_loader"
delegate_to: "localhost" # ❌ Not allowed
run_once: true # ❌ Not allowed✅ Correct - Apply attributes to tasks inside the loader:
# Inside log_loader.yml
- name: "Output log message"
ansible.builtin.debug:
msg: "{{ log_message }}"
delegate_to: "localhost" # ✅ CorrectWhen creating new loaders, use these as templates:
- Container orchestration: roles/container/tasks/docker_compose_loader.yml
- Package management: roles/native/tasks/package_loader/main.yml
- Service control: roles/native/tasks/service_loader/main.yml
- File operations: roles/native/tasks/file_loader.yml
- Read ansible.md and ansible-local.md
- Ensure all new loaders have Simple + Comprehensive usage examples
- File header with Purpose, Simple Usage, and Comprehensive Usage
- All modules use FQCN (ansible.builtin., community., containers.*)
- Task names use double quotes and os_fingerprint suffix
- No
delegate_to/run_onceoninclude_role/include_tasks - Conditionals use
is definedand explicit| boolconversion - Shell/command tasks have
changed_whenandfailed_when - Privilege escalation uses
become: "{{ become_enabled }}" - English-only comments in code files
- Standards: .agent/rules/ansible.md | .agent/rules/ansible-local.md
- Examples: roles/ - Every loader is self-documenting
MIT License - See LICENSE for details
This framework implements enterprise-grade Ansible patterns refined through production deployments across Alpine, Debian, and RHEL ecosystems. The Unified Loader System architecture enables consistent, maintainable infrastructure automation at scale.
Status: ✅ Production-Ready | 🎯 Reference Implementation
