Onboard an Ansible delivery environment

Audience: operators onboarding a non-Kubernetes environment whose deployment is owned by an existing Ansible playbook.

Use this path when Stella Ops must govern a release while an operator-supplied playbook remains the deployment engine. Stella Ops selects the digest-pinned release, applies gates and approvals, dispatches deploy.ansible to an assigned agent, supplies bounded target data and deployment-scoped credentials, and records the result. It does not generate, rewrite, or vendor the customer’s playbook tree.

Choose a different engine when the workload does not need Ansible:

Prerequisites

Run the examples from the repository root in PowerShell 7:

$baseUrl = "https://stella-ops.local"
$tenant = "<tenant-id>"
$headers = @{
    Authorization = "Bearer $token"
    "X-Stella-Tenant" = $tenant
}

1. Confirm the agent and playbook contract

List active agents and select the agent that owns the target network path:

$agents = Invoke-RestMethod `
    -Uri "$baseUrl/api/v1/release-orchestrator/agents?status=Active" `
    -Headers $headers `
    -SkipCertificateCheck

$agents | Select-Object id, name, status, capabilities, lastHeartbeatAt
$agentId = "<ansible-agent-guid>"

The chosen row must have a recent heartbeat and include Ansible in capabilities. Confirm these playbook inputs before creating the target:

deploy.ansible accepts VAULT_ADDR, VAULT_TOKEN, the documented deployment/registry keys, and ANSIBLE_HOST_KEY_CHECKING by exact key — ANSIBLE_HOST_KEY_CHECKING is the only approved ANSIBLE_* key. It rejects reserved STELLA_* and STELLAOPS_*, every other ANSIBLE_* key, and unknown environment keys. If your playbooks read their own namespaced variables, opt those prefixes in on the agent host with Agent:Ansible:Environment:AllowedKeyPrefixes (environment form Agent__Ansible__Environment__AllowedKeyPrefixes) — it is empty by default, so no key is accepted on a prefix alone, and it applies to every deployment that agent runs rather than to one deployment. A prefix inside the reserved STELLA_*/STELLAOPS_* or ANSIBLE_* namespaces would never match, because those checks run before any prefix is consulted; configuring one fails the agent host at startup rather than being silently ignored. Prefer extra-vars for non-secret playbook parameters that do not need to be process environment values.

2. Create or resolve the environment

Resolve the environment by natural key. Create it only when the GET returns 404; do not create duplicates.

$environmentName = "<environment-name>"
try {
    $environment = Invoke-RestMethod `
        -Uri "$baseUrl/api/v1/release-orchestrator/environments/by-name/$environmentName" `
        -Headers $headers `
        -SkipCertificateCheck
}
catch {
    if ($_.Exception.Response.StatusCode.value__ -ne 404) { throw }

    $environmentBody = @{
        name = $environmentName
        displayName = "<environment display name>"
        description = "Ansible delivery environment"
        orderIndex = 0
        isProduction = $false
        requiredApprovals = 0
        requireSeparationOfDuties = $false
        deploymentTimeoutSeconds = 1800
    } | ConvertTo-Json

    $environment = Invoke-RestMethod `
        -Method Post `
        -Uri "$baseUrl/api/v1/release-orchestrator/environments" `
        -Headers $headers `
        -ContentType "application/json" `
        -Body $environmentBody `
        -SkipCertificateCheck
}
$environmentId = $environment.id

Set production approval and protection settings to the customer’s governance policy. The sample deliberately creates a non-production environment; copying it must not weaken an existing production gate.

3. Prepare managed artifacts

Managed artifacts are operator-supplied, non-secret text stored with the target. They remove machine-local inventory/vars bind mounts and database edits while keeping the playbook itself outside Stella Ops source.

$inventory = @"
[app]
app-01 ansible_host=192.0.2.10

[all:vars]
ansible_connection=ssh
ansible_python_interpreter=/usr/bin/python3
"@

$varsTemplate = Get-Content -LiteralPath "C:\operator\customer\vars.$environmentName.j2" -Raw
$managedArtifacts = @{
    "inventory/hosts" = $inventory
    "vars/vars.$environmentName.j2" = $varsTemplate
    "compose/docker-compose.yml" = Get-Content -LiteralPath "C:\operator\customer\docker-compose.yml" -Raw
}

The keys must be portable relative paths using /. Rooted paths, .., backslashes, empty segments, and platform-invalid characters are rejected. The current agent bounds the payload to 64 files, 1,048,576 characters per file, and 8,388,608 characters in total. Anyone allowed to read target configuration can retrieve this text, so never place tokens, passwords, private keys, or rendered secret files in managedArtifacts.

Use these two runtime references:

The agent deletes the task directory after execution, including failure paths. The persisted target remains the durable source of the non-secret text.

{managedArtifactRoot} contains only the files explicitly stored above. If an unchanged playbook treats that token as its deployment-source directory, include every non-secret base template, Compose definition, or other source file it reads from that directory. A target containing only an environment override cannot satisfy a playbook that also expects a base template and Compose file.

4. Create or update the AnsibleHost target

Store secret references in the target, not their resolved values:

$targetName = "<target-name>"
$connectionConfig = @{
    type = "ansible_host"
    playbookBasePath = "/customer-infra"
    inventoryPath = "managed://inventory/hosts"
    managedArtifacts = $managedArtifacts
    defaultPlaybookPath = "playbooks/deploy.yml"
    sshUsername = "deployment"
    privateKeySecretRef = "vault://<provider-id>/secret/<environment>#SSH_PRIVATE_KEY"
    becomeMethod = "sudo"
    becomePasswordSecretRef = "vault://<provider-id>/secret/<environment>#BECOME_PASSWORD"
}

try {
    $target = Invoke-RestMethod `
        -Uri "$baseUrl/api/v1/release-orchestrator/environments/$environmentId/targets/by-name/$targetName" `
        -Headers $headers `
        -SkipCertificateCheck

    $updateBody = @{
        displayName = "<target display name>"
        connectionConfig = $connectionConfig
        agentId = $agentId
    } | ConvertTo-Json -Depth 8

    $target = Invoke-RestMethod `
        -Method Put `
        -Uri "$baseUrl/api/v1/release-orchestrator/targets/$($target.id)" `
        -Headers $headers `
        -ContentType "application/json" `
        -Body $updateBody `
        -SkipCertificateCheck
}
catch {
    if ($_.Exception.Response.StatusCode.value__ -ne 404) { throw }

    $createBody = @{
        name = $targetName
        displayName = "<target display name>"
        type = "AnsibleHost"
        connectionConfig = $connectionConfig
        agentId = $agentId
    } | ConvertTo-Json -Depth 8

    $target = Invoke-RestMethod `
        -Method Post `
        -Uri "$baseUrl/api/v1/release-orchestrator/environments/$environmentId/targets" `
        -Headers $headers `
        -ContentType "application/json" `
        -Body $createBody `
        -SkipCertificateCheck
}
$targetId = $target.id

Optional fields include vaultPasswordSecretRef, registryTunnelHost, registryTunnelPort, and registryTunnelHostKeyFingerprint. When useAgentRegistry=true, set registryTunnelHost to the concrete inventory host that the agent must reach. For a multi-host inventory, constrain the deployment with ansible.limit; one reverse tunnel cannot represent several different hosts. Pin the tunnel host key where the operator has an authoritative fingerprint.

Target reads mask secret-reference fields. On update, preserve masked fields or send new references through the supported API; do not replace a masked value with an empty string.

5. Bind the release component contract

The target supplies defaults. Component configuration can override them with:

Example component override:

$releaseId = "<draft-release-guid>"
$componentId = "<release-component-guid>"
$configOverrides = @{
    "ansible.playbook" = "playbooks/deploy.yml"
    "ansible.inventory" = "managed://inventory/hosts"
    "ansible.limit" = "app"
    "ansible.extraVars.DEPLOYMENT_SOURCE_PATH" = "{managedArtifactRoot}"
    "ansible.extraVars.ENVIRONMENT_NAME" = $environmentName
    "ansible.environment.VAULT_ADDR" = "https://vault.internal.example"
    "ansible.environment.VAULT_TOKEN" = "builtin://deploy/$environmentName/vault-token"
}

$patchBody = @{ configOverrides = $configOverrides } | ConvertTo-Json -Depth 6
Invoke-RestMethod `
    -Method Patch `
    -Uri "$baseUrl/api/v1/release-orchestrator/releases/$releaseId/components/$componentId" `
    -Headers $headers `
    -ContentType "application/json" `
    -Body $patchBody `
    -SkipCertificateCheck

In pull mode, Stella Ops mints a deployment-scoped capability. Network references using builtin://, vault://, openbao://, or authref:// remain references until the agent presents that capability to the orchestrator broker; the agent has no standing provider token. Inline plain:, file://, and base64: values do not get that edge-resolution behavior and must not be used for production secrets.

The orchestrator also adds deterministic release, target, job, component image, and component digest extra-vars. The playbook may consume them, but must not substitute a tag for the supplied digest-pinned image.

6. Check readiness without deploying

$check = Invoke-RestMethod `
    -Method Post `
    -Uri "$baseUrl/api/v1/release-orchestrator/targets/$targetId/health-check" `
    -Headers $headers `
    -SkipCertificateCheck

$check

Expected: success is true. For AnsibleHost, this proves the assigned agent exists, is active, has a fresh heartbeat, and advertises Ansible. It does not prove that the playbook will converge on the destination host.

Before deployment, separately verify:

7. Deploy and capture forcing-function evidence

Promotion and deployment mutate the target environment. Run them only with the environment owner’s authorization:

stella release show $releaseId
stella release deploy $releaseId --reason "Ansible delivery to $environmentName" --json

Do not accept the HTTP response or an agent heartbeat as completion. Required evidence is:

Record the command, deployment id, target id, agent id, terminal status, and independent destination verification in the owning sprint evidence. Do not copy bearer tokens or resolved credentials into evidence.

Failure modes

Target health reports a missing, stale, or incapable agent

Repair the existing agent identity and capability configuration. Do not register a replacement identity merely to bypass a stale target assignment. Rebuild the agent image when Ansible runtime dependencies are absent; recreating the same image cannot add packages or Galaxy collections.

ansible-playbook cannot find inventory or vars

Confirm the inventory uses managed://<artifact-key>, the artifact key exists, and {managedArtifactRoot} is used only in environment/extra-var values. Do not point the playbook at a workstation path or add a manual database edit.

Environment key is rejected

Move ordinary non-secret parameters to ansible.extraVars.*. If a new process environment key is genuinely required by a general deployment contract, add it through a reviewed source change with adjacent tests and documentation; do not bypass the allow-list.

Broker resolution is denied

Confirm the secret reference belongs to the release’s computed allowed-reference set and the deployment is using pull mode with a minted capability. A customer Vault integration is read-only from Stella’s perspective; it is not used to mint Stella’s temporary deployment capability.

Playbook runs but the application is unhealthy

Separate delivery convergence from application dependency health. Prove the digest landed and the requested service started, then check application-specific database, cache, messaging, routing, and plugin dependencies. Do not mark the deployment engine broken solely because a delivered application cannot reach an external dependency.

Implementation references