---
title: "Ansible"
description: "Run Ansible through Alpacon with the alpacax.alpacon collection. Configure a service token, its account and access rules, and handle approvals and results."
---

> Full documentation index: https://docs.alpacax.com/llms.txt

# Ansible

Use the `alpacax.alpacon` collection to run playbooks through Alpacon. The `alpacax.alpacon.exec` connection uses the API directly; it does not require the Alpacon CLI.

## Prepare the controller and token

The controller needs ansible-core 2.15 or later and Python 3.11 or later. Install the collection:

```bash
ansible-galaxy collection install alpacax.alpacon
```

On a macOS controller, set this before running Ansible:

```bash
export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES
```

Prepare an [application and service token](/use/iam/applications/service-tokens/):

1. Bind a server account to the application, or set its username and enable provisioning. The first command provisions the account on a Linux server with a current agent. Use that account as `ansible_user`.
2. Give the application a global role covering the token's scopes. Commands need `command:create` and `command:read`; server-name lookup also needs `server:read`. Grant file-upload access for module copies, and download access for tasks that fetch files.
3. Request an `alpst-` service token and have a Superuser or the request's approver approve it in the web console. Command and file-transfer scopes always wait for approval. Confirm that the token is active and enabled.
4. After approval, add a server ACL for each target and file ACLs for the paths and directions the playbook uses. Ordinary modules are copied to the server before execution, so they need both command and upload permission.
5. On Alpacon Cloud, have a **Command ACL admins** member add the commands under the token's **Pre-approved commands**. This access is separate from the workspace role. Choose **Connect** if asked to connect. On self-hosted deployments, add command ACL rules matching the lines Ansible submits.

File transfer requires S3-backed storage. Allow the temporary paths used for module uploads as well as the paths your tasks manage. These token operations need no work session. See the [setup and API reference](/reference/api/#automate-with-a-service-token) for the account, role, and ACL steps.

Set `ansible_user` explicitly. The connection defaults to `root`, which a service token cannot use. Set it to the application's server account, such as `ci-deploy`.

## Run a minimal playbook

Save this as `inventory.ini`, replacing the server UUID and account with your values:

```ini
[web]
web-01 alpacon_server_id=<server-uuid>

[web:vars]
ansible_connection=alpacax.alpacon.exec
ansible_user=ci-deploy
```

Set the workspace URL and inject the approved service-token key from your secret store. The value below is a placeholder:

```bash
export ALPACON_URL=https://your-workspace.us1.alpacon.io
export ALPACON_TOKEN='alpst-REPLACE_WITH_SERVICE_TOKEN'
```

Save this as `playbook.yml`:

```yaml
- hosts: web
  gather_facts: false
  tasks:
    - name: Who am I
      ansible.builtin.raw: whoami
```

Run it:

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

The command runs as `ci-deploy`. The `raw` task submits `whoami` without uploading a module, so a command rule for `whoami` covers this check. Direct root access is off by default. Leaving `ansible_user` at its default produces a `400` response containing `"code": "workspace_direct_root_disabled"`. The error body has this shape (`...` stands for the validation messages):

```text
{"code": "workspace_direct_root_disabled", "field_errors": {"non_field_errors": [...]}}
```

Only in a workspace that allows direct root does this request reach the `application_system_user_not_found` check. Changing an ACL to allow `root` does not make it an application-owned account.

### How Ansible runs modules

An ordinary module task submits wrapper commands to find the account home, create a random directory under `~/.ansible/tmp/`, discover Python, set permissions, execute the uploaded module, and remove the temporary files. For example, the command module uploads `AnsiballZ_command.py`. Each submitted wrapper line needs a matching command rule, and the module upload needs a file ACL for its temporary path. A narrow rule for `whoami` does not cover those module tasks. Wrapper lines vary with Ansible and the target; inspect them in a controlled live run before using the playbook in production.

## Connection options

Set options as inventory variables, environment variables, or in `ansible.cfg` where supported. The INI names below belong under `[alpacon_connection]`, except `remote_user`, which belongs under `[defaults]`.

| Inventory variable | Environment variable | INI name | Default or purpose |
| --- | --- | --- | --- |
| `alpacon_url` | `ALPACON_URL` | `url` | Required workspace URL |
| `alpacon_token` | `ALPACON_TOKEN` | `token` | Required `alpst-` service token |
| `alpacon_server_id` | — | — | Exact server UUID; takes precedence over a name |
| `alpacon_server_name` | — | — | When omitted, `ansible_host` if set, otherwise the inventory hostname |
| `alpacon_approval_timeout` | `ALPACON_APPROVAL_TIMEOUT` | `approval_timeout` | `300` seconds; `0` or less fails immediately when approval is needed |
| `alpacon_ssl_verify` | `ALPACON_SSL_VERIFY` | `ssl_verify` | `true` |
| `alpacon_ca_cert` | `ALPACON_CA_CERT` | `ca_cert` | CA certificate path |
| `ansible_user` or `ansible_ssh_user` | — | `remote_user` | Defaults to `root`; set the application's account |

Without a server ID, the connection looks up the name. More than one match produces `Multiple servers match name ...`; no visible match produces `Server '<name>' not found in alpacon.`. Use a server ID to resolve ambiguity, and check the token's server access if the target is not found.

## Privilege escalation with become

With `become`, Ansible still connects as the application's account and submits a sudo command. On Alpacon Cloud, service-token sudo is allowed below Critical, including unassessed commands, and refused at Critical unless the workspace is Advisory. In Advisory, it is not refused for risk at any level, and the decision is recorded. Outside Advisory, a workspace `token_exec` rule can tighten the risk threshold. The workspace sudo setting must be on; otherwise sudo is refused with `workspace_sudo_with_mfa_disabled`. On a default self-hosted installation, every service-token sudo is refused with `sudo_command_not_authorized`. Sudo policies do not apply to service tokens, and sudo does not wait for a human reviewer.

The command entry must cover the submitted line, including its sudo prefix. Ansible adds temporary paths and a generated marker, so review the actual line when configuring command access. Do not grant a broad `sudo *` entry just to make a playbook pass. `become` is unavailable with this token-only connection on a default self-hosted installation.

## Approval waits and results

When a command waits for approval, the connection prints:

```text
alpacax.alpacon.exec: command <id> is awaiting approval in the alpacon console; waiting up to <n>s for a decision
```

Have an approver review it in the web console. `alpacon_approval_timeout` controls that wait separately from the 300-second execution timeout.

| Result | Return code and output |
| --- | --- |
| Success | `rc: 0` |
| Execution timeout | `rc: 124`, stderr `Command execution timed out` |
| Other non-success result | `rc: 1`, with the status such as `denied`, `rejected`, or `awaiting_approval` on stderr |
| Denied or rejected | `rc: 1`, stdout `Command was rejected by the approval policy` |
| Approval wait timeout | `rc: 1`, stderr `awaiting_approval`; stdout warns `do not resubmit, that would queue a duplicate run` |

On Cloud, `Command was rejected by the approval policy` can also mean a missing command rule. The API accepts the command with `201`, then it ends `rejected` with `rejection_cause` `command_acl_denied` or `command_acl_unavailable`. Read `rejection_cause` on the command before changing approval settings. On self-hosted deployments, a command ACL refusal returns `403 api_token_acl_not_allowed`.

If the approval wait times out, do not resubmit: the original command remains queued, and another submission can run it twice. Review the existing command in the console.

Module tasks submit several commands, and result polling also consumes requests. Even a small fleet can reach the default service-token limit of `1000/hour`. The connection does not retry HTTP `429`. Reduce concurrency and wait for the `Retry-After` delay. Before retrying a task, check whether its command was already accepted; a polling failure does not mean the command failed, and resubmitting it can duplicate execution.

For HTTP errors, expired credentials, and refused accounts, see [Automation failure modes](/integrate/automation-failure-modes/).

Source: https://docs.alpacax.com/integrate/ansible/
