---
name: write-ansible-role
description: "Generate a production-grade Ansible role (app-deploy via Docker Compose, or infra-service via systemd/binaries/packages) from a Role Design Spec, following senior conventions: full defaults, orchestrator tasks, env/compose or systemd templates, healthchecks, registry, air-gap, migration jobs, README and Molecule examples. Use when you have a spec and need a deployable role for an application or infrastructure component."
license: MIT
handoffs:
  - label: "Review the generated role"
    agent: "review-ansible-role"
    prompt: "Run the review-ansible-role skill against the generated role at {target}/{role} to QA it before shipping. Use the same Role Design Spec as context."
---

# What I do

I turn a **Role Design Spec** (from `analyze-ansible-role-project` and/or
`analyze-ansible-component`) into a complete, deployable Ansible role. I follow
the senior conventions in `CONVENTIONS.md` in this folder and adapt the
boilerplate in `templates/`:

- `templates/role/` — app-deploy (Docker Compose) boilerplate.
- `templates/infra-service/` — systemd / native-binary boilerplate.

I produce a role that is: idempotent, parameterized via `defaults/main.yml`,
air-gap capable, registry-aware (app-deploy), healthcheck-verified,
multi-instance capable, and documented.

I support **any application runtime** — .NET, Go, Python, Node, etc. — and both
role shapes:

- `app-deploy` — an application as Docker Compose services.
- `infra-service` — a standalone component installed as binaries/packages with
  a systemd unit (kafka, clickhouse, k3s, node_exporter, minio, ...).

# When to use me

- You have a Role Design Spec (or enough product info) and want a role written.
- You need an `app-deploy` role (a product = several compose services) or an
  `infra-service` role (one standalone service like k3s / clickhouse / kafka).

If there is **no spec yet**, first run `analyze-ansible-role-project` (and
`analyze-ansible-component` for each infra dependency) so I do not guess.

# User Input

> You **MUST** consider the user input before proceeding (if not empty).

`$ARGUMENTS` may contain any of:

1. A Role Design Spec — path to `role-design-spec.md` or paste its content.
2. Target directory for the generated role (default: next to the spec / in the
   roles directory the user chooses).
3. Any explicit overrides (deploy folder, registry, nginx mode, role type,
   target runtime).

If the spec is missing from the input, **ask for it** — never invent a role shape
from nothing.

**Interaction rule**: an entirely missing spec → **ask**. A spec present but with a
missing detail (a specific image tag, port, config key, install URL) → **do not block
or guess**: proceed with an empty / `changeME` placeholder and list the item under
**Open Questions** in the output summary.

# Operating Constraints

- Follow `CONVENTIONS.md` **without deviation** unless the spec demands otherwise.
- Do not guess image tags, ports, config keys, or install URLs — the spec is
  the single source of truth. A missing detail does not block the run: leave it
  as an empty / `changeME` placeholder and list it under **Open Questions** in
  the output summary. Only an entirely missing spec warrants a question back to
  the user.
- Role must be idempotent and multi-instance capable by default.
- app-deploy: air-gap capable and registry-aware. infra-service: systemd unit
  + verified `active`.
- No hardcoded values that should be defaults; secrets are placeholders
  (`changeME`) documented as "override via vault/env".
- Keep files self-contained: render the whole role tree, not a partial one.

# Execution Steps

## 1. Load this folder first

- `CONVENTIONS.md` — the conventions this role must follow (naming, defaults
  order, task flow, compose/env or systemd patterns, static-IP scheme, air-gap).
- `templates/` — boilerplate: `role/` for app-deploy, `infra-service/` for
  standalone services.

## 2. Constitution Check

Cross-check the spec against `CONVENTIONS.md` **before writing anything**:

- Role type is `app-deploy` or `infra-service` (Conventions §1).
- Variable prefix derived correctly (`<role>_`, Conventions §2).
- Any spec value that contradicts a convention must be flagged to the user as a
  deviation and **explicitly confirmed** before proceeding.

## 3. Determine the shape

- `app-deploy`: generate the full compose tree below.
- `infra-service`: generate the systemd/package tree below; skip the product
  compose split.

## 4. Generate the role tree

### app-deploy

Create every file under `<target>/<role>/`:

```
<role>/
├── defaults/main.yml
├── tasks/
│   ├── main.yml                  # orchestrator (order per CONVENTIONS §5)
│   ├── infrastructure.yml        # app-deploy only
│   ├── <component>.yml           # one per app component
│   ├── nginx-conf.yml            # in-client nginx config (when nginx_in_client)
│   ├── logging.yml               # when logging_enable
│   ├── healthcheck.yml
│   ├── verify.yml                 # optional HTTP endpoint check (when verify_enable)
│   ├── host-nginx.yml            # when host nginx needed
│   └── airgap.yml                # when air-gap supported
├── handlers/
│   └── main.yml                  # nginx reload/restart handlers (app-deploy)
├── templates/
│   ├── base.env.j2               # shared env include (base-dotnet / base-python ...)
│   ├── <component>.env.j2        # per-component env
│   ├── nginx.conf.j2             # in-client nginx (when nginx_in_client)
│   ├── docker/infrastructure.yml.j2   # app-deploy, split mode
│   ├── docker/<role>-app.yml.j2       # app-deploy, split mode
│   ├── docker/docker-compose.yml.j2   # app-deploy, single mode (1 app + <=1 infra)
│   ├── sites/<server_name>.conf.j2    # host nginx site
│   └── healthcheck.sh.j2
├── files/                        # static configs (vector.yaml, init sql)
├── molecule/default/             # test scenario (molecule.yml, converge.yml,
│   │                             #   group_vars/all.yml, tests/test_<role>.py)
├── meta/main.yml
├── README.md
└── examples/
    ├── playbook.yml              # ready-to-run playbook
    └── requirements.yml          # galaxy roles + collections (community.docker)
```

### infra-service

```
<role>/
├── defaults/main.yml             # version, URLs, dirs, user/group, tuning
├── vars/main.yml                 # OS-family values (deps, repo URLs)
├── tasks/
│   ├── main.yml                  # orchestrator (order per CONVENTIONS §10)
│   ├── prepare.yml               # user/group, dirs
│   ├── install.yml               # dispatcher: install-archive.yml OR install-{{ ansible_os_family }}.yml
│   ├── install-archive.yml       # get_url + unarchive + version marker (when install_method == "archive")
│   ├── install-Debian.yml        # apt/repo install (when install_method == "package")
│   ├── install-RedHat.yml        # yum/dnf install (when install_method == "package")
│   ├── configure.yml             # render *.conf.j2 from templates
│   └── service.yml               # systemd unit, daemon-reload, enable, restart
├── templates/
│   ├── <service>.service.j2      # systemd unit
│   └── <service>.conf.j2         # app config
├── molecule/default/             # test scenario (molecule.yml, converge.yml,
│   │                             #   group_vars/all.yml, tests/test_<role>.py)
├── handlers/main.yml             # service reload/restart handlers
├── meta/main.yml
├── README.md
└── examples/
    └── playbook.yml
```

Adapt from `templates/role/` (app-deploy) or `templates/infra-service/`; substitute
the tokens and **place each template file at the exact path the generated tasks
reference**. The abstract filenames in `templates/` are not the final paths.

### Tokens

| Token | Replaced by | Used in |
| ----- | ----------- | ------- |
| `__PREFIX__` | variable prefix (`role` with `-` → `_`) | all |
| `__ROLE__` | role directory name (kebab-case) | all |
| `__PRODUCT__` | product display name | README / meta / systemd |
| `__AUTHOR__` | galaxy author | meta/main.yml |
| `__COMPOSE_PREFIX__` | short container-name prefix (app-deploy) | compose / names |
| `__NET__` | docker network name prefix (app-deploy) | defaults |
| `__PRODUCT_DB__` | default DB name (app-deploy) | defaults |
| `__REGISTRY_PATH__` | base registry image path (app-deploy) | defaults |
| `__MIGRATE_COMMAND__` | one-shot migration CLI (app-deploy) | compose job |
| `__COMPOSE_FILE__` | compose file the role deploys (`<app>-app.yml` or `docker-compose.yml`, app-deploy) | molecule test |
| `__VERSION__` | pinned version (infra-service) | defaults / install |
| `__VARIANT__` | runtime variant, e.g. kafka scala (infra-service) | defaults |
| `__DIST_URL__` | distribution base URL (infra-service) | defaults |
| `__PACKAGE__` | archive/package filename (infra-service) | defaults |
| `__BINARY__` | installed binary name (infra-service) | systemd unit |

Substitute every token in every file; a leftover `__...__` in generated output
is a bug.

### Template → target mapping

The writer places each abstract template at the path referenced by the tasks:

| Abstract file (`templates/role/`) | Final role path |
| -------------------------------- | --------------- |
| `defaults-main.yml` | `defaults/main.yml` |
| `tasks-main.yml`, `tasks-airgap.yml`, `tasks-host-nginx.yml`, `tasks-certbot.yml`, `tasks-infrastructure.yml`, `tasks-component.yml` (copy per component), `tasks-nginx-conf.yml`, `tasks-logging.yml`, `tasks-healthcheck.yml`, `tasks-verify.yml` | `tasks/<same-name>.yml` (`tasks/component.yml` → one file per component, e.g. `tasks/client.yml`, `tasks/backend.yml`) |
| `handlers-main.yml` | `handlers/main.yml` |
| `meta-main.yml` | `meta/main.yml` |
| `nginx.conf.j2` | `templates/nginx.conf.j2` |
| `backend.env.j2`, `client.env.j2` | `templates/<component>.env.j2` |
| `templates-compose-app.yml.j2` | `templates/docker/<compose-prefix>-app.yml.j2` (split) |
| `templates-compose-infrastructure.yml.j2` | `templates/docker/infrastructure.yml.j2` (split) |
| `templates-compose-all.yml.j2` | `templates/docker/docker-compose.yml.j2` (single) |
| `templates-sites-conf.j2` | `templates/sites/<server_name>.conf.j2` |
| `templates-healthcheck.sh.j2` | `templates/healthcheck.sh.j2` |
| `files/vector.yaml` | `files/vector.yaml` |
| `molecule/default/*` | `molecule/default/*` |
| `examples-playbook.yml`, `examples-requirements.yml` | `examples/playbook.yml`, `examples/requirements.yml` |
| `README.md` | `README.md` |

| Abstract file (`templates/infra-service/`) | Final role path |
| ---------------------------------------- | --------------- |
| `defaults-main.yml`, `vars-main.yml` | `defaults/main.yml`, `vars/main.yml` |
| `tasks-main.yml`, `tasks-prepare.yml`, `tasks-install.yml`, `tasks-install-archive.yml`, `tasks-install-Debian.yml`, `tasks-install-RedHat.yml`, `tasks-configure.yml`, `tasks-service.yml` | `tasks/<same-name>.yml` |
| `handlers-main.yml` | `handlers/main.yml` |
| `meta-main.yml` | `meta/main.yml` |
| `service.service.j2` | `templates/<role>.service.j2` |
| `service.conf.j2` | `templates/<role>.conf.j2` |
| `service.env.j2` | `templates/<role>.env.j2` |
| `molecule/default/*` | `molecule/default/*` |
| `examples-playbook.yml` | `examples/playbook.yml` |
| `README.md` | `README.md` |

## 5. Fill in per-component details

- defaults: complete sections per CONVENTIONS §3, compute connection strings.
- tasks: orchestrator order per §5 (app-deploy) or §10 (infra-service);
  per-component tasks per §6.
- templates: compose per §7 (x-logging anchor, static IPs, healthcheck,
  depends_on, one-shot jobs). Choose `compose_mode` per §7's single-compose
  rule — `single` when the app is one component + at most one infra service
  (render `docker/docker-compose.yml.j2`), otherwise `split`
  (`docker/infrastructure.yml.j2` + `docker/<role>-app.yml.j2`), env per §8
  (mapping to the app's own config discovery — appsettings sections,
  os.Getenv, python-dotenv, env.sample.js — `{% if %}` blocks,
  `| string | lower`).
- infra-service: systemd unit + config per §10.
- molecule: generate the default scenario per §11 (docker driver, systemd image,
  converge includes the role, group_vars fills required placeholders, testinfra
  asserts the deployed state). Ensure the host-group name in `molecule.yml`
  matches the one used by `tests/`.
- air-gap: per §9 (app-deploy).

# Validation

Before presenting the result, verify **all** of the following:

- **Var completeness**: every `{{ var }}` used in tasks/templates is defined in
  `defaults/main.yml` (grep to confirm).
- **YAML**: every `.yml`/`.yaml` file parses; `.j2` templates render without
  template syntax errors.
- **Env correctness**: env keys match the app's real config discovery (the
  spec's env config map), not invented.
- **Lint**: `yamllint` (see `review-ansible-role` skill for the full QA pass).
- **Syntax**: `ansible-playbook --syntax-check` if ansible-core is available.
- **Molecule**: `molecule/default/` present; `molecule converge` + `molecule
  verify` pass when a Docker driver is available (or is documented as
  CI-deferred).

Fix any failure before output. If you cannot run the tools, self-review with
the checklist from `review-ansible-role/CHECKLIST.md`.

# Output

A complete role directory. The role must be runnable with:

```bash
ansible-playbook -i inventory playbook.yml
```

Present a summary: role type, target path, what was filled from the spec, and
any open questions or assumptions made.

# Quality bar

- Passes the Constitution Check (no silent deviations).
- Zero hardcoded values that should be defaults.
- Required vars (license keys etc.) abort early with a clear message.
- app-deploy: compose files, healthchecks, registry login/logout, and air-gap
  are consistent and mutually aware.
- infra-service: systemd unit installed, service verified `active`.
- README documents every variable and shows basic + advanced playbooks.
