- Create systemd service templates for llama_exporter, nvidia_exporter, and podman. - Add Prometheus configuration template for GPU metrics scraping. - Introduce variables for GPU monitoring role in main.yml. - Implement tasks for syncing llama models, including user setup and package installation. - Update podman tasks to ensure proper sudo access and lingering for the podman user. - Modify inventory to include gpu_monitoring group. - Add Terraform modules for deploying Vaultwarden, including ingress and service configurations. - Create Grafana dashboard for real-time GPU monitoring metrics. - Update secrets and environment files to include Vaultwarden admin token.
12 KiB
name, description
| name | description |
|---|---|
| ansible-develop | Use when developing, writing, or debugging Ansible playbooks and roles in this repository. Covers playbook and role conventions, inventory group targeting, molecule-podman testing, variable scoping (defaults/vars/host_vars), Jinja2 templates, tag-based execution, and known patterns such as the podman shared tasks helper and dual Debian/RedHat OS-family blocks. |
Ansible Playbook & Role Skill
Purpose
Use this skill when creating new playbooks or roles in ansible/. It covers
project conventions for structure, testing, variable management, secrets, and
execution patterns used across all existing automation.
Repository Layout
ansible/
ansible.cfg # inventory = ../inventory/hosts
requirements.yml # collections: ansible.posix, community.general,
# containers.podman, community.libvirt, ...
.yamllint.yml # 120-char line limit, extended default rules
playbook_*.yml # top-level entry points (run from repo root)
tasks/ # shared task fragments (e.g. tasks/podman.yml)
roles/
<name>/
defaults/main.yml # public defaults (overridable by callers)
vars/main.yml # internal values (non-overridable)
tasks/main.yml # entry point; include_ or block per sub-feature
handlers/main.yml # notify handlers (services, reloads)
templates/ # Jinja2 files (.j2 extension)
files/ # static files copied as-is
meta/main.yml # galaxy metadata, min_ansible_version, dependencies
molecule/default/ # tests: converge.yml, prepare.yml, verify.yml, molecule.yml
tests/inventory # small static inventory for role tests
tests/test.yml # molecule verifier
.yamllint.yml # (optional) role-level yamllint overrides
README.md # role documentation
inventory/
hosts # host groups (ini-format inventory)
host_vars/ # per-host YAML variable files
Playbooks live in ansible/ and are run from the repository root. The
inventory path is relative: ../inventory/hosts (as declared in
ansible/ansible.cfg).
Inventory & Group Targeting
The inventory at inventory/hosts uses ini-format groups. Playbooks target
groups via hosts: <groupname>. Group names follow kebab-case convention.
Existing groups:
| Group | Hosts |
|---|---|
all |
prod-01, dev-01, vh-01, gpu-01 |
hardening |
prod-01, dev-01, vh-01, gpu-01 |
ubuntu |
prod-01, dev-01 |
fedora |
vh-01, gpu-01 |
microk8s |
prod-01, dev-01 |
monitoring |
prod-01 |
duo |
prod-01, dev-01, vh-01, gpu-01 |
virtualization |
vh-01 |
ollama |
gpu-01 |
comfyui |
gpu-01 |
microk8s |
prod-01, dev-01 |
certbot |
vh-01, gpu-01 |
nvidia |
gpu-01 |
amd |
gpu-01 |
public |
prod-01 |
private |
dev-01, vh-01, gpu-01 |
windows |
ci-01 |
When adding a new playbook, add a new group in inventory/hosts and target
it from the playbook.
Playbook Patterns
Two patterns are used. Choose the one that fits the task.
Pattern 1 — Role-based (preferred for reusable logic)
Use when the task is encapsulated in a role or may be reused across playbooks.
---
- name: My feature setup
hosts: mygroup
gather_facts: true
roles:
- role: "my_role"
tags:
- roles
- roles::my_role
Pattern 2 — Inline tasks
Use for single-shot playbooks that configure one specific host with custom logic (e.g., virt_host, encryption).
---
- name: My inline setup
hosts: mygroup
become: true
gather_facts: true
tasks:
- name: Ensure the system is Debian
ansible.builtin.assert:
that:
- ansible_os_family == 'Debian'
fail_msg: "This playbook only supports Debian hosts."
- name: Install required packages
become: true
ansible.builtin.apt:
name:
- package-one
- package-two
state: present
update_cache: true
Shared task fragments
Complex operations (e.g. podman pod/container lifecycle) go into tasks/ and
are included via ansible.builtin.include_tasks. See tasks/podman.yml for
the pattern: required-var checks, OS-family set_fact, become_user blocks,
and loop-based resource creation.
Role Development
Minimal role directory structure
roles/my_role/
defaults/main.yml # public defaults
tasks/main.yml # entry point
handlers/main.yml # notify handlers
templates/ # Jinja2 files
meta/main.yml # galaxy metadata
molecule/default/ # tests
defaults/main.yml
Declare all public variables here with safe defaults. This is how callers override behavior.
---
my_feature_enabled: true
my_feature_port: 8080
my_feature_users: []
Use lookup('env', 'VAR_NAME') for secrets with empty default:
my_api_key: "{{ lookup('env', 'MY_API_KEY') | default('') }}"
tasks/main.yml
Use block + when for OS-family conditional logic:
---
- name: Install packages (Debian)
when: ansible_os_family == 'Debian'
block:
- name: Install deps
become: true
ansible.builtin.apt:
name: "{{ item }}"
state: present
update_cache: true
loop:
- package-one
- name: Install packages (RedHat)
when: ansible_os_family == 'RedHat'
block:
- name: Install deps
become: true
ansible.builtin.dnf:
name: "{{ item }}"
state: present
loop:
- package-one
Use include_tasks for sub-features, tagged separately:
- name: Include volume provisioning
ansible.builtin.include_tasks: volume.yml
tags:
- roles::my_role::volume
- name: Include KMS setup
ansible.builtin.include_tasks: kms.yml
tags:
- roles::my_role::kms
meta/main.yml
Always declare galaxy_info with namespace a13labs, author
Alexandre Pires, and company A13Labs:
---
galaxy_info:
author: Alexandre Pires
description: Role to do something
company: A13Labs
role_name: my_role
namespace: a13labs
license: MIT
min_ansible_version: "2.1"
Tag conventions
Use nested tags following the pattern roles::<role_name>::<feature>. Top-level
playbooks use roles and roles::<role_name>.
Molecule Testing (Podman)
Every role should have a molecule/default/ test scenario using the Podman
driver.
molecule.yml
---
driver:
name: podman
platforms:
- name: instance
image: ubuntu:latest
pre_build_image: true
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:ro
privileged: true
provisioner:
name: ansible
env:
MY_ROLE_VAR: "test_value"
playbooks:
converge: converge.yml
prepare: prepare.yml
verifier:
name: ansible
prepare.yml
Must install python3 and sudo using raw (facts are unavailable in containers):
---
- name: Prepare
hosts: all
gather_facts: false
tasks:
- name: Update cache
ansible.builtin.raw: apt update
- name: Install required packages
ansible.builtin.raw: apt install -y python3 sudo
converge.yml
---
- name: Converge
hosts: all
gather_facts: true
tasks:
- name: Include my_role
ansible.builtin.include_role:
name: "my_role"
Testing
Run from the role directory:
cd ansible/roles/my_role
molecule test
This runs: prepare -> converge -> verify -> destroy.
Important: Most containers lack systemd. Guard service-related tasks:
when: ansible_service_mgr == "systemd".
Secrets & Environment Variables
Secrets are injected via sectool using sectool.json at the repository root.
The ANSIBLE_USER env var must be set before opening opencode.
Secrets in roles use lookup('env', 'VAR_NAME') — sectool injects them
automatically when playbooks are run through sectool exec.
Pattern in role defaults:
api_key: "{{ lookup('env', 'MY_API_KEY') | default('') }}"
Pattern in host_vars (for host-level secrets):
cloudns_auth_id: "{{ lookup('env', 'CLOUDNS_AUTH_ID') }}"
cloudns_auth_password: "{{ lookup('env', 'CLOUDNS_PASSWORD') }}"
Never hardcode secrets. Always use lookup('env', ...) with a | default('')
fallback.
Execution
All Ansible commands must be run from the ansible/ directory using
sectool to inject secrets:
cd ansible/
# Role-based playbook
sectool -f ../sectool.json exec ansible-playbook -u $ANSIBLE_USER playbook_myfeature.yml
# Tag-based partial run
sectool -f ../sectool.json exec ansible-playbook -u $ANSIBLE_USER playbook_myfeature.yml --tags roles::my_role::volume
# Limit to specific group
sectool -f ../sectool.json exec ansible-playbook -u $ANSIBLE_USER playbook_myfeature.yml --limit mygroup
# Role tests (molecule runs independently, no sectool wrapper needed)
cd ansible/roles/my_role && molecule test
$ANSIBLE_USER must be set in the shell before opening opencode.
Linting
yamllint
cd ansible
yamllint -c .yamllint.yml .
Rules: 120-char line width (warning), forbid implicit/explicit octal, extended default ruleset.
ansible-lint
If available, run from the ansible/ directory:
cd ansible
ansible-lint
Known Patterns
Dual OS-family support
Always support both Debian and RedHat families when packages differ:
- name: Set binary path (Debian)
when: ansible_os_family == 'Debian'
ansible.builtin.set_fact:
podman_binary: /usr/bin/podman
- name: Set binary path (RedHat)
when: ansible_os_family == 'RedHat'
ansible.builtin.set_fact:
podman_binary: /usr/sbin/podman
become_user blocks
When a task needs to run as a different user (e.g. podman operations), use a
become: true + become_user block:
- name: Run tasks as podman user
become: true
become_user: "{{ podman_user }}"
block:
- name: Build images
containers.podman.podman_image:
...
systemd service generation
Use ansible.builtin.template to generate .service files from .j2
templates, then daemon_reload + systemd enable:
- name: Create systemd service file
become: true
ansible.builtin.template:
src: myservice.service.j2
dest: "/etc/systemd/system/my-service.service"
mode: "0644"
notify: Reload daemon
handlers:
- name: Reload daemon
changed_when: false
ansible.builtin.systemd:
daemon_reload: true
nmcli bridge creation
Network bridge configuration uses nmcli connection add with async +
wait_for to handle reconnection after interface changes:
- name: Create bridge
ansible.builtin.command: nmcli connection add ...
register: bridge_created
async: 10
poll: 0
- name: Wait for reconnection
ansible.builtin.wait_for:
host: "{{ bridge_ip }}"
port: 22
timeout: 30
delegate_to: localhost
Review Checklist
Before proposing a new playbook or role:
- Playbook targets a defined group in
inventory/hosts(add one if needed). - Role has
defaults/main.ymlwith all public variables + safe defaults. - Secrets use
lookup('env', 'VAR') | default(''), never hardcoded. - Dual OS-family support when package names differ.
- Tasks tagged with
roles::<role_name>::<feature>. - Molecule
molecule/default/scenario with prepare + converge. molecule.ymluses Podman driver withprivileged: true+ cgroup mount.- Line length within 120 characters.
yamllint -c ansible/.yamllint.yml ansible/passes.- Service tasks guarded by
when: ansible_service_mgr == "systemd"in molecule containers.