Why Ansible?
Ansible handles system-level configuration that requires root privileges:Package Management
APT packages, Snap packages, external repositories
System Settings
Sudo configuration, system hardening
Desktop Environment
GNOME preferences, power management
Idempotency
Safe to run multiple times, only changes what’s needed
While chezmoi manages files in your home directory, Ansible manages the system environment those files run in.
Data-Driven Architecture
The Traditional Problem
Most Ansible setups create a separate role for each application:- Role proliferation (hundreds of tiny roles)
- Duplicate package installation logic
- Difficult maintenance
- Slow playbook execution
The Data-Driven Solution
This project uses a universalcommon role that reads configuration from centralized data files:
Configuration File Structure
group_vars/all.yml
This is the single source of truth for all software installations:ansible/group_vars/all.yml
Data Structure Breakdown
- external_repositories
- workstation_packages
- snap_packages
Defines third-party APT repositories:
| Field | Purpose |
|---|---|
name | Repository identifier (used for filename) |
key_url | URL to GPG signing key |
repo | APT source line (supports Jinja2 variables) |
keyring | Where to store the de-armored GPG key |
The
common role automatically downloads the GPG key, de-armors it using gpg --dearmor, and configures the APT source.The Common Role
Thecommon role is the universal installer engine that processes the data from group_vars/all.yml.
Task Breakdown
Download and De-Armor GPG Keys
- Downloads ASCII-armored GPG keys
- Converts to binary format (required for modern APT)
- Uses
createsfor idempotency (won’t re-download)
Playbook Structure
site.yml
The main playbook is remarkably simple:ansible/site.yml
Playbook Options Explained
Playbook Options Explained
hosts: local: Targetslocalhost(defined inansible.cfg)connection: local: Don’t use SSH, run directlybecome: true: Escalate to root (usessudo)tags: Allow selective execution (--tags common)
Running the Playbook
The playbook is automatically invoked by chezmoi:run_once_after_ansible.sh.tmpl
Adding New Software
Example: Adding Docker
When to Create a New Role
Only create dedicated roles for:Complex Configuration
Complex Configuration
Example: GNOME desktop settingsRequires multiple
dconf tasks with specific key-value pairs.Multi-Step Setup
Multi-Step Setup
Example: Database server initialization
- Install package
- Create database users
- Import schema
- Configure authentication
Alternative Package Managers
Alternative Package Managers
Example: Installing from source
- Download tarball
- Extract and compile
- Install to
/usr/local/
Ansible Configuration
ansible.cfg
ansible/ansible.cfg
inventory
ansible/inventory
localhost as the only managed host.
Testing and Linting
Ansible Lint
The project usesansible-lint to enforce best practices:
Common Lint Rules
- risky-shell-pipe
- yaml[truthy]
- fqcn[action]
Problem: Using pipes in shell tasks without
set -o pipefailIntegration Tests
Thetests/test-packages.sh script verifies installations:
Idempotency Guarantees
Ansible tasks are designed to be safe to run multiple times:| Task | Idempotency Mechanism |
|---|---|
| GPG key download | args.creates - skips if keyring exists |
| APT repository | state: present - only adds if missing |
| Package install | state: present - skips if already installed |
| Sudoers file | copy module - only writes if content differs |
| GNOME settings | dconf - only changes if value differs |
You can run
chezmoi apply (which triggers Ansible) as many times as you want. It will only make changes when needed.Best Practices
Data Organization
Data Organization
- Keep
group_vars/all.ymlalphabetically sorted - Group related packages together with comments
- Use consistent naming (kebab-case for repo names)
Repository Management
Repository Management
- Always specify
signed-byin repo strings - Use modern keyring locations (
/usr/share/keyrings/) - De-armor GPG keys (binary format is required)
Task Writing
Task Writing
- Use fully qualified module names (
ansible.builtin.*) - Add
set -o pipefailto shell tasks with pipes - Use
true/false, notyes/no - Include
tagsfor selective execution
Testing
Testing
- Run
ansible-lintbefore committing - Update integration tests when adding packages
- Test in a VM or container before production
Architecture Benefits
This data-driven approach provides: ✅ Scalability: Add 100 packages by editing one file✅ Maintainability: One role to update instead of dozens
✅ Performance: Single APT transaction instead of sequential installs
✅ Clarity: Configuration is data, not code
✅ Flexibility: Easy to fork and customize for different machines
Next Steps
Add Packages
Learn how to add your own software
Secrets Management
Understand Bitwarden integration
GNOME Role
Customize desktop environment settings
Ansible Docs
Official Ansible documentation