Files
a13labs.infra/.opencode/skills/ansible-develop/SKILL.md
T
alexandre.pires 350650ecc2 Add GPU monitoring services and configurations
- 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.
2026-05-29 23:42:14 +02:00

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.yml with 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.yml uses Podman driver with privileged: 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.