Skip to content

Run Ansible playbooks with Forgeplane

Forgeplane Ansible v1 runs ansible-playbook from a published template version on workers that you operate. Ansible uses the shared queue, worker-pool selection, input validation, catalog secrets, log stream, and audit record. It is execute-only and does not use Terraform or OpenTofu plan promotion, teardown, managed state, or drift workflows.

An Ansible template supports the execute operation. Forgeplane rejects plan, preview, apply, destroy, and teardown before the playbook process starts. Ad-hoc Ansible commands are not part of the public v1 contract.

A published template version captures the input schema and Ansible runtime settings. Use a commit SHA for the template Git ref when you require source repeatability. A branch or movable tag can resolve to different repository content during a later execution.

Set Ansible runtime options in the published template version’s variables object:

{
"ansible_playbook": "playbooks/site.yml",
"ansible_inventory_input": "targets",
"ansible_limit": "web:&production",
"ansible_tags": ["bootstrap", "deploy"],
"ansible_skip_tags": ["destructive"],
"ansible_output_path": ".forgeplane/outputs.json",
"ansible_ssh_private_key_secret": "ssh_key",
"ansible_known_hosts_secret": "known_hosts",
"ansible_vault_password_secret": "vault_password",
"ansible_secret_vars": {
"service_api_token": "api_token"
}
}

ansible_playbook selects a repository-relative playbook. Set it explicitly for a new template. It is required when the configured template path contains more than one top-level .yml or .yaml file. A legacy template with exactly one top-level playbook remains unambiguous. Absolute paths and paths that escape the checked-out template are rejected.

Use these optional controls when needed:

  • ansible_limit supplies the Ansible host-pattern limit.
  • ansible_tags selects tags as a string or list.
  • ansible_skip_tags skips tags as a string or list.
  • ansible_output_path selects the structured-output manifest path.

Forgeplane resolves environment, run, pre-run resolver, and Assembly inputs against the published template input schema. The worker writes the resolved input object to a mode-0600 JSON file and passes it with --extra-vars @file. JSON booleans, numbers, arrays, objects, and nulls keep their types.

Choose one inventory source:

  • ansible_inventory selects a repository-relative inventory file or an inline host list such as localhost,.
  • ansible_inventory_input selects one top-level typed run input.

The two settings are mutually exclusive. An inventory input can contain an inventory document string, a JSON inventory object, or an array of host names. Forgeplane converts a host-name array to all.hosts.

Ansible credential settings contain secret slot IDs, not plaintext values. Declare each slot as a top-level secret field in the template input schema or pre-run resolver contract, then bind it through the Forgeplane secret catalog.

The supported bindings are:

  • ansible_ssh_private_key_secret for an SSH private key;
  • ansible_known_hosts_secret for trusted host keys;
  • ansible_vault_password_secret for an Ansible Vault password; and
  • ansible_secret_vars for a map from Ansible variable names to secret slot IDs.

A catalog-backed SSH private key requires a known_hosts binding. Forgeplane enables strict host-key checking and confines OpenSSH to the materialized file. Raw ssh_private_key and vault_password execution variables are rejected.

The worker creates keys, known-hosts data, Vault passwords, secret extra-vars, generated inventories, and ordinary extra-vars inside the isolated run workspace with mode 0600. The configured IaC UID/GID owns the files, and workspace cleanup removes them after execution.

Forgeplane redacts materialized values and their non-empty lines from streamed stdout and stderr. This does not make deliberate disclosure safe. A playbook must not encode, transform, print, or copy credentials into logs or ordinary output.

Logs and structured outputs are separate. A playbook can write a JSON object to ansible_output_path. Forgeplane supplies the absolute destination as the reserved extra variable forgeplane_output_path:

- name: Publish Forgeplane outputs
ansible.builtin.copy:
dest: "{{ forgeplane_output_path }}"
mode: "0600"
content: >-
{{ {
"parameters": {"address": service_address},
"artifacts": {"report": {"path": "reports/deploy.json"}},
"secrets": {"credential": "secret://project/service-credential"}
} | to_json }}

The v1 manifest limit is a fixed 1 MiB. Larger manifests fail validation. Only parameters, artifacts, and secrets object channels are accepted, and secret-channel leaves must be secret:// references. Forgeplane rejects a manifest that contains a materialized plaintext secret.

When ansible_output_path is configured, the manifest is required. Without that setting, Forgeplane reads .forgeplane/outputs.json when the file exists and otherwise completes without structured output.

The base worker image intentionally contains no infrastructure tools. Use the version-matched Ansible worker image:

ghcr.io/forgeplane-io/forgeplane-worker-ansible:<forgeplane-version>

The image pins ansible-core and includes OpenSSH. Treat the pool image as the execution environment. Extend it with version-pinned collections, roles, Python packages, and system packages that your playbooks require.

The current Helm chart renders worker images as repository:tag. Set the exact Forgeplane release tag. Do not put an image digest in the repository field.

workerPools:
ansible:
enabled: true
image:
repository: ghcr.io/forgeplane-io/forgeplane-worker-ansible
tag: "<forgeplane-version>"
pullPolicy: IfNotPresent
capabilities:
- ansible
extraEnv:
- name: FORGEPLANE_MANAGED_STATE_ENVELOPE_WRITE_VERSION
value: "1"
- name: FORGEPLANE_WORKER_IAC_UID
value: "10001"
- name: FORGEPLANE_WORKER_IAC_GID
value: "10001"
- name: FORGEPLANE_WORKER_POOL_NAME
value: ansible
toolCache:
persistence:
enabled: false

A pool-level extraEnv list replaces workerDefaults.extraEnv; it does not append to it. Keep the envelope setting and the positive IaC UID/GID when you add the pool name. The chart map key also does not set FORGEPLANE_WORKER_POOL_NAME by itself.

Set the Ansible template registry entry’s default worker pool to ansible. If a run explicitly supplies requested_worker_pool_name, that value must also be ansible and match the template default; it cannot replace or override the default. A matching pool name and the advertised ansible capability are both required for scheduling.

The root worker supervisor uses the default CHOWN, DAC_OVERRIDE, FOWNER, SETUID, and SETGID capabilities only to prepare and clean private workspace files and transfer ownership. The ansible-playbook child process runs as the configured IaC UID/GID, not as root. Keep the default worker security context unless an equivalent tested image and runtime contract replaces it.

For local evaluation, use the repository’s docker-compose.ansible.yml profile. See Docker Compose deployment and Helm deployment for the surrounding runtime contract.

Operate retries, cancellation, and requeue safely

Section titled “Operate retries, cancellation, and requeue safely”

A failure before ansible-playbook starts can use the worker’s remaining retry budget. A non-zero exit after process start is non-retryable because the playbook might already have changed remote systems. In an Assembly, max_attempts is the explicit persisted rerun mechanism.

The coordinator reaper can also return an assigned run to queued when the worker goes offline or its lease expires. That recovery does not prove that an old playbook process stopped. Design playbooks to be idempotent, pin the source ref and worker image, and protect target-side operations against concurrent execution.

Canceling a queued run prevents it from starting. For an assigned or running run, the coordinator records cancellation, but that status alone does not prove process termination on the worker. Confirm worker and target-system quiescence before retrying.

Explicit requeue is available only from failed, canceled, or timed_out. It reuses the same run ID, clears prior execution state, and returns the run to queued. Requeue only after you confirm that no earlier process can still change the target. See Run operations for the shared lifecycle.

An ordinary execute run is queued after normal input, policy, permission, quota, and worker-admission checks. It does not create Terraform-style plan approval. Put any required pre-execution approval in the surrounding operational workflow.