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:
- use a
ComposeHosttarget for Stella-native Compose delivery, including the local Docker Desktop path or the documented SSH mode; - use
deploy.docker-pluginwhen the task is specifically to extract a plugin tree from a digest-pinned container and optionally restart local containers.
Prerequisites
- The playbook tree is already operator-reviewed and available at the same
playbookBasePathinside the assigned agent runtime. Managed artifacts are for small target-specific text files, not for distributing the playbook tree. - An active target agent advertises the
Ansiblecapability and its capability health check can runansible-playbook --version. Runtime packages and required Galaxy collections must already be present in the agent image; do not install dependencies during a deployment. - The destination hosts are reachable from the agent, and the playbook’s SSH user and privilege-escalation behavior are known.
- Secret providers are configured. See multi-provider secret operations and ADR-034.
- Every release component has an immutable image digest. Tag-only image identity is not accepted as release truth.
- You have a bearer token with
orch:readandorch:operate, plus the normal release/promotion permissions. Keep it in the current shell only.
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:
- base path visible inside the agent, for example
/customer-infra; - playbook path relative to that base, for example
playbooks/deploy.yml; - SSH username and inventory host/group;
- environment variables read by the unchanged playbook;
- extra variables passed with
--extra-vars; - target-specific inventory and vars-template text;
- secret references, never plaintext secret values.
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:
managed://inventory/hostsresolves to the task-scoped inventory file;{managedArtifactRoot}inside approved environment or extra-var values resolves to the task-scoped root directory.
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:
ansible.playbookorplaybook;ansible.inventoryorinventory;ansible.limit/limitandansible.tags/tags;ansible.extraVars.<NAME>,ansible.extraVar.<NAME>, orextraVars.<NAME>;ansible.environment.<NAME>oransible.env.<NAME>.
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:
- the operator playbook path exists inside the agent;
- the inventory host is reachable from the agent;
- every referenced secret is present and permitted for the tenant/environment;
- every release component has a digest;
- required approvals and evidence gates are satisfied.
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:
- terminal deployment status
succeeded; deploy.ansibletask assigned to the intended agent and target;- the unchanged playbook reached its convergence checks;
- the destination reports the expected digest-pinned containers/files/services;
- no secret values appear in task stdout, stderr, raw-output proof, or error text;
- the task-scoped managed-artifact directory is absent after completion.
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
- Release Orchestrator architecture
- Execution plugin contract
- Secret reference grammar
- API-only onboarding helper
