A self-contained guide (docs/reference/playbook-authoring.md) to the contract between a playbook and Steward's run UI — the `# description:` comment, vars/vars_prompt → fill-in fields, secret naming, hosts: all targeting, managed credentials, and idempotency. Includes a "what Steward reads" summary table, the metadata/extensibility note (only `# description:` today; reserved `# steward:<key>:` namespace for future keys), and an annotated example. Meant to be fed to another session authoring Steward-friendly playbooks. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6.7 KiB
Authoring Steward-friendly Ansible playbooks
This is the contract between a playbook and Steward's run UI. Follow it and a playbook drops into Steward with a description, fill-in variable fields, correct targeting, and credentials supplied automatically — no per-playbook wiring.
It applies to any playbook in a configured source (bundled, the writable local source, or a git source), including third-party ones.
1. Describe what it does — # description:
Steward shows a one-line description when a playbook is selected in the run form.
---
# description: Reclaim disk on Docker/Swarm nodes by pruning unused images and build cache.
- name: Docker system prune
hosts: all
...
- Format: a comment line
# description: <text>anywhere in the file. First match wins. Case-insensitive on thedescription:key. - Why a comment and not a key: Ansible rejects unknown play keys (you can't add
description:to a play), so a comment is the portable place. It survivesansible-playbookuntouched. - Fallback: if there's no
# description:comment, Steward uses the first play'sname:. So always give your play a meaningfulname:even without the comment. - Keep it to one readable line. Longer "how to use" notes can go in additional
normal comments — Steward only reads the
description:line.
2. Declare tunables in vars: — they become fill-in fields
Every scalar entry in a play's vars: block becomes an editable field in the
run form, with the default shown as the input's placeholder.
vars:
prune_all_images: false # → checkbox-ish text field, placeholder "default: false"
keep_last_days: 7 # → field, placeholder "default: 7"
registry_url: "" # → field, placeholder "no default"
- Blank field = use the default. Steward only sends fields the operator actually fills, so an untouched field falls through to the playbook/inventory default rather than overriding it.
- Only scalars (string/int/float/bool) surface as fields. Lists/dicts are skipped — set those in the playbook or via inventory group/host vars.
- Values are delivered as extra-vars (
-e), which are the highest precedence in Ansible — they override thevars:defaults. (This is why the default can be empty and still be safely overridden at run time.)
vars_prompt: also works
Steward reads vars_prompt too. Use it when you want an explicit prompt or a
required value:
vars_prompt:
- name: release_tag
prompt: "Which release to deploy?" # shown as the field label
# no default → Steward marks the field REQUIRED
- name: admin_password
prompt: "Admin password"
private: true # → masked field, never stored
3. Secrets — name them so they're masked and not persisted
A field is treated as secret (rendered masked, and its value is never written to the DB / run history) when either:
- the variable name contains
password,passwd,secret,token,api_key/apikey,private_key, orcredential(case-insensitive), or - it's a
vars_promptentry withprivate: true(Ansible's default for vars_prompt is private).
So name sensitive variables accordingly (db_password, api_token,
vault_secret) and they're handled safely with no extra config. Non-secret
run-time vars are persisted (so scheduled runs can reuse them); secret ones are
passed to the run only.
4. Target with hosts: all
Steward builds the inventory itself from the target / group the operator picks in the run form (or the single host on a host page). Your play should:
hosts: all # run against whatever Steward scoped to
- Don't hardcode hostnames or rely on a checked-in inventory for Steward runs (Steward generates a fresh inventory per run).
- Per-host connection vars (
ansible_host, plus anything you set on the target in Ansible → Inventory) arrive as inventory host vars. - The run form's Limit / Tags map to
--limit/--tags.
5. Privileges & connection — don't put credentials in the playbook
Steward supplies SSH and become for you:
- Steady-state runs connect as the managed
stewardaccount using Steward's managed key; that account has passwordless sudo. So just usebecome: truewhere you need root. - First-contact provisioning uses a one-time bootstrap user/password the operator enters (never stored).
- Never embed SSH keys, passwords, or
ansible_user/ansible_ssh_passin the playbook. Connection identity is global (Settings → Ansible) or per-target.
6. Be idempotent
Steward re-runs playbooks (updates, schedules, retries). Use modules that
converge (state-based) rather than ad-hoc command:/shell: where possible, so
re-runs are safe.
What Steward reads from a playbook (summary)
| Source in the playbook | What Steward does with it |
|---|---|
# description: <text> comment |
Description shown on selection (first match) |
first play name: |
Description fallback |
vars: scalar entries |
Run-time fill-in fields (placeholder = default) |
vars_prompt: entries |
Run-time fields (required if no default) |
secret-looking var name / private: true |
Field masked + value not persisted |
hosts: |
Expected to be all; Steward provides the inventory |
Everything else (SSH user/key, become password, the inventory, steward_token
etc. for the agent playbooks) is injected by Steward at run time.
Metadata convention & extensibility
Today the only comment-based metadata Steward parses is # description:.
Everything else is structural (vars, vars_prompt, hosts, the play
name). If we add more comment metadata later, it will use a reserved
# steward:<key>: <value> namespace (e.g. # steward:category: maintenance,
# steward:confirm: true for a run-confirmation gate) so it can't collide with
ordinary comments. These extra keys are not implemented yet — only
# description: is. Don't author playbooks that depend on # steward:* keys
until this doc says they're live.
Minimal annotated example
---
# description: Restart a systemd service and confirm it came back up.
- name: Restart a service
hosts: all
become: true
gather_facts: false
vars:
service_name: "" # required-ish: operator fills it in the run form
tasks:
- name: Validate input
ansible.builtin.assert:
that: service_name | default('') | length > 0
fail_msg: "Set service_name."
- name: Restart
ansible.builtin.systemd:
name: "{{ service_name }}"
state: restarted
- name: Confirm active
ansible.builtin.command: "systemctl is-active {{ service_name }}"
changed_when: false