The module lifecycle stage: Experimental
The module has requirements for installation
Before enabling the module, you need to allow the use of experimental modules in the deckhouse ModuleConfig. To do this, set the allowExperimentalModules parameter to true.
This guide explains how to create, configure and troubleshoot runs of the ansible module. A run executes an Ansible playbook against Deckhouse Platform (DP) virtual machines (VMs) or against hosts specified by address.
Module resources
The module adds two custom resources to the cluster, which are listed in the table below.
| Resource | Purpose |
|---|---|
| AnsibleRun | A run that executes a playbook once on the target hosts and records the result in its status. The spec of a run cannot be changed, so create a new object to execute the playbook again |
| AnsibleRunSchedule | A cron schedule that creates AnsibleRun objects the way a CronJob resource creates Job objects |
This guide uses the following terms:
- Run: An AnsibleRun object.
- Playbook: The YAML file with tasks that the run executes.
- Runner Pod: A temporary Pod where DP executes the
ansible-playbookutility.
The Secret with credentials, the ConfigMap with the playbook text and the target VirtualMachine resources have to be in the namespace of the run. The only object a run can use from outside its namespace is a cluster-wide ClusterNetwork.
Quick start
The steps below create a first run. It checks connectivity to a virtual machine (VM) and records the result in its status.
-
Prepare the VM. To connect, it needs an SSH server, a login user and an address DP knows about. The following cloud-init fragment installs the SSH server and creates such a user:
#cloud-config packages: - openssh-server users: - name: ansible lock_passwd: false sudo: ALL=(ALL) NOPASSWD:ALL ssh_authorized_keys: - ssh-ed25519 <SSH_PUBLIC_KEY> ansible@example runcmd: - systemctl enable --now sshHere
<SSH_PUBLIC_KEY>is the public key of the pair whose private key you put into the Secret. -
Wait until the VM reaches the
Runningphase and receives an address:d8 k get vm demo-vm-01 -n demo \ -o jsonpath='{.status.phase}{" "}{.status.ipAddress}{"\n"}'Here
demo-vm-01is the name of your VM, anddemois its namespace. -
Create a Secret with SSH credentials in the namespace of the VM:
d8 k create secret generic ssh-creds -n demo \ --from-literal=username=ansible \ --from-file=ssh-privatekey=./id_ed25519Here
./id_ed25519is the path to the private key file of the same pair as<SSH_PUBLIC_KEY>. -
Create a run that checks connectivity to the VM:
apiVersion: ansible.deckhouse.io/v1alpha1 kind: AnsibleRun metadata: name: configure-vm namespace: demo spec: target: type: VirtualMachines virtualMachines: names: [demo-vm-01] connection: secretRef: name: ssh-creds playbook: type: Inline inline: | --- - name: Configure VM hosts: all tasks: - name: Check SSH connectivity ansible.builtin.ping:The
namesfield holds the name of the VM from the previous steps. Selecting VMs by labels is described in Virtual machines. -
Check the result of the run:
d8 k get ansibleruns -n demoExample output:
NAME PHASE REASON HOSTS OK FAILED UNREACHABLE SKIPPED AGE configure-vm PlaybookSucceeded PlaybookSucceeded 1 1 0 0 0 42sThe detailed result, including per-host task counters, is recorded in the status of the run:
d8 k get ansiblerun configure-vm -n demo -o yaml
Run structure
A run manifest is made of four blocks, which are listed in the table below. Each block is described in a section of its own.
| Block | Purpose | Required |
|---|---|---|
target |
Defines which hosts the run configures | Yes |
connection |
Defines how the run connects to the hosts | Yes |
playbook |
Defines the playbook source, the tags of the part to execute and the variables | Yes |
runner |
Defines the execution mode of the playbook | No |
The manifest below has every block of a run filled in:
apiVersion: ansible.deckhouse.io/v1alpha1
kind: AnsibleRun
metadata:
name: configure-db
namespace: demo
spec:
target:
type: VirtualMachines # VirtualMachines | Hosts
virtualMachines:
selector: # Either selector or names.
matchLabels:
role: db
connection:
secretRef:
name: ssh-creds # SSH credentials from the namespace of the run.
network: # Optional. The main network by default.
type: ClusterNetwork # Main | Network | ClusterNetwork
name: vlan-64
playbook:
type: Inline # Inline | ConfigMap | Git
inline: |
---
- hosts: all
tasks:
- ansible.builtin.ping:
tags: [deploy] # Optional. Execute only the tasks with these tags.
skipTags: [migrations] # Optional. Leave out the tasks with these tags.
vars: # Optional. Passed as --extra-vars.
- name: app_version
value: "1.4.2" # Any YAML value: string, number, list, mapping.
- name: db_password
valueFrom:
secretKeyRef:
name: app-secrets
key: db-password
varsFiles: # Optional. YAML documents with variables.
- configMapRef:
name: app-config # The key defaults to vars.yaml.
runner:
dryRun: false # Ansible check mode.
diff: false
verbosity: 0 # From 0 to 4.Targets
The target block defines the hosts the playbook is executed on, that is, DP virtual machines (VMs) or hosts specified by address. One run works with hosts of a single kind only, so the virtualMachines and hosts fields cannot be set in the same object.
This block replaces the -i parameter of the Ansible command line. From it, DP builds the inventory file that you would otherwise have to write and keep up to date by hand. Host addresses are resolved when the run starts, so a VM recreated with another address is configured without any change to the run manifest.
Virtual machines
VMs are selected by labels or named explicitly.
The example below selects VMs by label:
target:
type: VirtualMachines
virtualMachines:
selector:
matchLabels:
role: demoAn example of selecting VMs by a label expression:
target:
type: VirtualMachines
virtualMachines:
selector:
matchExpressions:
- key: role
operator: In
values: [web, db]An example of selecting VMs by name:
target:
type: VirtualMachines
virtualMachines:
names: [demo-vm-01, demo-vm-02]The selector and names fields are mutually exclusive. The API server rejects a manifest with an empty selector. Such a selector would match every VM in the namespace, and the action of a run cannot be undone.
If a VM matches the selection but cannot be configured when the run starts, it is listed in status.skippedHosts with a reason, and the playbook is executed on the remaining VMs. The skip reasons are listed in Troubleshooting.
Hosts specified by address
To configure a host whose address DP does not know, use a target of type Hosts and specify the address in the run manifest. DP does not know addresses configured by hand inside the guest OS or assigned by an external DHCP server, nor the addresses of physical servers, network appliances and hosts from other clusters.
Targets of type Hosts are available in commercial DP editions only. In DP Open the API server rejects a manifest with such targets when it is created.
An example of a run with hosts specified by address:
target:
type: Hosts
hosts:
- address: 192.168.55.10
vars:
- name: app_role
value: frontend
groups: [web]
- address: db.example.comA host specified by address has no VirtualMachine resource, so the run treats it differently from a DP VM. The differences are listed in the table below.
| Property | VirtualMachines |
Hosts |
|---|---|---|
| Address | Resolved by DP | Specified in the run manifest |
| Target phase | Waits for Running or Migrating |
Not applicable |
| Check for another active run | Performed, the skip reason is TargetBusy |
Not performed |
| Variables and groups | Annotations on the VM | The vars and groups fields |
status.hosts[].name |
The name of the VM | The address of the host |
status.skippedHosts |
Filled in when VMs are skipped | Always empty |
DP does not check whether another run uses a host specified by address, so two runs can work on the same address at the same time. Do not start runs that configure the same host concurrently.
The vars field of such a host is declared like host variables, valueFrom sources included. Group names follow the rules described in Ansible inventory.
Ansible inventory
DP builds the inventory file from the selected targets and from the annotations of the VMs. DP sets the ansible_host and ansible_user variables and the credential paths itself, and you cannot override them, while every other variable is set in VM annotations and in the run manifest.
For a VM, host groups and variables come from its annotations, which are listed in the table below.
| Annotation | Purpose |
|---|---|
ansible.deckhouse.io/groups |
A comma-separated list of groups. The host joins each of the listed groups in addition to the all group |
vars.ansible.deckhouse.io/<VARIABLE_NAME> |
A host variable named <VARIABLE_NAME> |
An example of a VM with such annotations:
apiVersion: virtualization.deckhouse.io/v1alpha2
kind: VirtualMachine
metadata:
name: demo-vm-01
namespace: demo
annotations:
ansible.deckhouse.io/groups: "web,production"
vars.ansible.deckhouse.io/app_role: "frontend"The annotations of a VM play the same role as the host_vars/<HOST_NAME> file in an ordinary Ansible project. Unlike that file, they are stored on the VM itself. For a host specified by address, the vars and groups fields in the run manifest set the same data.
In both cases DP builds an inventory file of the following form:
all:
hosts:
demo-vm-01:
ansible_host: 10.66.10.2
ansible_user: ansible
ansible_ssh_private_key_file: /home/runner/.ssh/ssh-privatekey
app_role: frontend
web:
hosts:
demo-vm-01: {}
production:
hosts:
demo-vm-01: {}Variable and group names in annotations follow the same rules as in the run manifest (they are described in Refused variable names). In addition, an annotation may not set variables with the ansible_ prefix, except ansible_port. The reasons for this restriction are described in Permissions and security.
An SSH port belongs to a particular VM, so set a non-standard port with an annotation on that VM:
metadata:
annotations:
vars.ansible.deckhouse.io/ansible_port: "2222"The port of a host specified by address is set in the hosts[].vars field. The spec.playbook.vars field sets the port for every host of the run at once and overrides both of these values, because it has a higher precedence.
Do not set ansible_port in the spec.playbook.vars field if the VMs of the run use different SSH ports.
DP ignores annotations whose names do not start with the ansible.deckhouse.io/groups or vars.ansible.deckhouse.io/ prefix and skips empty group entries. If an annotation breaks a naming rule, the run ends in the Error phase with the InvalidInventoryAnnotations reason before DP creates the runner Pod. The status message names the VM and the annotation that broke the rule.
Connection
The connection block defines over which transport, with which credentials and through which network a run connects to its hosts.
Transport
The transport is set in the connection.type field. SSH is currently the only available value and is used by default, so the field can be omitted. An example with the transport set explicitly:
connection:
type: SSH
secretRef:
name: ssh-credsSSH credentials
A run takes the credentials for the connection from the Secret whose name is set in the connection.secretRef field:
connection:
secretRef:
name: ssh-credsThe Secret has to be in the namespace of the run and contain the keys listed in the table below. At least one of the ssh-privatekey and password keys is required.
| Key | Purpose |
|---|---|
username |
SSH user. If the key is not set, DP does not set the ansible_user variable in the inventory |
ssh-privatekey |
SSH private key |
password |
SSH password |
become-password |
Password for privilege escalation, used by tasks with the become: true parameter |
ansible-vault-password |
Password for Ansible Vault. Passed to the ansible-playbook utility as the vault password file |
The key names follow the built-in kubernetes.io/ssh-auth and kubernetes.io/basic-auth Secret types.
The command below creates a Secret that holds a password instead of a private key:
d8 k create secret generic ssh-creds-password -n demo \
--from-literal=username=ansible \
--from-literal=password=<PASSWORD>Here <PASSWORD> is the password of the SSH user.
Networks
By default a run is executed in the main network. The runner Pod works from the cluster pod network. It connects to a virtual machine (VM) at the address from the status.ipAddress field of the VirtualMachine resource.
Additional networks are available in commercial DP editions only. In DP Open the API server rejects a manifest with any network type other than Main when it is created.
A VM can also be attached to additional networks of the sdn module, a project one (Network) or a cluster-wide one (ClusterNetwork). Each such network forms a separate L2 domain, and the pod network has no route into it. So if the SSH server of the VM is reachable only in an additional network, the address of the VM alone is not enough, and the run has to be executed in the same network.
Requirements for an additional network
A run in an additional network works under the following conditions:
- The
sdnmodule is enabled in the cluster. Otherwise the run fails with theDependencyMissingreason. - The network has an address pool (
spec.ipam.ipAddressPoolRef), because both the VM and the runner Pod take their address from the pool. A run in a network without a pool ends in theErrorphase with theNetworkWithoutIPAMreason before DP creates the runner Pod. The status message names the network. - A DHCP client runs on the interface of this network in the guest OS. Without it, DP reports an address the VM does not answer at, and the host is
unreachable. - The interface configuration in the guest OS is bound to a MAC address or a predictable name rather than to
ethX. Attaching a network or reorderingspec.networkscan renumber theethXinterfaces, and the DHCP client then requests an address on the wrong interface. - The MTU of the network does not exceed the MTU of the node interfaces the network is built on. An MTU mismatch is hard to spot, because the SSH connection succeeds, but copying a file or installing a package hangs.
Configuring the network of a run
The network is named in the connection.network block. DP attaches the runner Pod to that network and takes the address of every target in it as well:
connection:
secretRef:
name: ssh-creds
network:
type: ClusterNetwork # Main (default) | Network | ClusterNetwork
name: vlan-64The network block mirrors an entry of the spec.networks list of the VM, so take its values from the manifest of the VM:
# VirtualMachine
spec:
networks:
- type: Main
- type: ClusterNetwork
name: vlan-64 # Name this network in the run.
- type: Network
name: storage-netA Network is visible only in its own namespace, while a ClusterNetwork is available from any. A run works in a single network only, so use labels to split VMs from different networks into several runs.
Set the network block for hosts specified by address as well, because a host address does not tell DP which network to attach the runner Pod to. This way you can configure hardware outside the cluster, such as physical servers, network appliances or hosts of other clusters, in a management network.
Runner Pod behaviour in an additional network
The additional interface does not replace the main one, so the runner Pod stays attached to the cluster pod network as well. The init containers still reach the cluster DNS and the cluster services, so a playbook from a Git repository inside the cluster and Galaxy content from a cluster mirror are fetched the same way as without an additional network.
An address pool that publishes routes is an exception. These routes apply inside the Pod, and a default route (0.0.0.0/0) sends all outbound traffic through the additional network. Requests to addresses inside the cluster keep working, because they have their own routes. But downloads from outside the cluster, for example from a public Git repository or from Galaxy, go through the additional network and may become unreachable. Before you start a run in such a network, check the list of routes in the spec.pools[].routes field of the pool.
A VM address appears in the status.networks[] list when the interface is actually attached, not when the address is allocated. An IPAddress resource can already exist for the VM while its status is still empty. Until the interface is attached, the run skips such a VM with the NoAddressInNetwork reason.
Size of the address pool
The pool has to cover both the VMs and the concurrent runs, because a run holds an address while its runner Pod exists. DP deletes the Pod of a successful run at once but keeps the Pod of a failed run until the AnsibleRun object is deleted. Delete failed runs once you investigate them, otherwise they keep holding addresses.
Playbook
The playbook block defines where the playbook comes from and which part of it to execute. The playbook can be set as text in the run manifest or taken from a ConfigMap resource or a Git repository. A single run uses one source only.
Inline playbook
The playbook text is set directly in the manifest of the run:
playbook:
type: Inline
inline: |
---
- name: Configure VM
hosts: all
tasks:
- ansible.builtin.ping:The size of an inline playbook is limited to 65,536 characters. Place a larger playbook in a ConfigMap resource or in a Git repository.
Playbook from a ConfigMap
To reuse a playbook in several runs or to get around the size limit of an inline playbook, store it in a ConfigMap in the namespace of the run. The run points at the key of that resource:
playbook:
type: ConfigMap
configMapRef:
name: demo-playbook
key: playbook.yaml # Default value.Playbook from a Git repository
The Git source fetches the whole repository tree, so a project with roles, a group_vars directory and templates is executed unchanged. An example of a run with a playbook from a Git repository:
playbook:
type: Git
git:
url: https://github.com/example-org/infra
revision: v1.4.0 # Branch, tag or SHA. The default branch if omitted.
path: playbooks/site.yml # Defaults to playbook.yaml.
secretRef:
name: git-token # For private repositories.These settings correspond to the following commands:
git clone --recurse-submodules https://github.com/example-org/infra && cd infra
git checkout v1.4.0
ansible-galaxy install -r requirements.yml # If the project declares dependencies.
ansible-playbook -i inventory.yml playbooks/site.ymlDP fetches the repository together with its submodules. If the repository contains a requirements.yml, roles/requirements.yml or collections/requirements.yml file, the Galaxy roles and collections declared in it are installed before the playbook starts. The commit that was actually executed is recorded in the status.playbookCommit field.
The repository and the Galaxy content are fetched through the proxy configured for the cluster, and the module has no proxy settings of its own. These settings do not apply to the tasks of the playbook, because Ansible executes the tasks on the target hosts over SSH. If a task needs a proxy, declare it in the environment parameter of that task.
Private repositories
The credentials of a private repository are stored in a separate Secret whose name is set in the git.secretRef field. They are unrelated to the SSH credentials for the target hosts. DP passes them to the checkout step only, and the tasks of the playbook never see them.
For URLs of the form ssh://git@… the Secret is required and has to contain the ssh-privatekey and known_hosts keys. The known_hosts key is needed because SSH checks the key of the Git server strictly, and its contents can be obtained with the ssh-keyscan <GIT_SERVER> command, for example ssh-keyscan github.com. An example of such a Secret:
apiVersion: v1
kind: Secret
metadata:
name: git-deploy-key
namespace: demo
type: kubernetes.io/ssh-auth
stringData:
ssh-privatekey: |
-----BEGIN OPENSSH PRIVATE KEY-----
...
known_hosts: |
github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5...For HTTP(S) URLs the Secret has to contain the username and password keys, where the access token serves as the password. For GitLab, set the username to oauth2. If the Git server uses a certificate of its own certificate authority, add the ca.crt key to the Secret as well. An example of a Secret with a token:
apiVersion: v1
kind: Secret
metadata:
name: git-token
namespace: demo
type: kubernetes.io/basic-auth
stringData:
username: oauth2
password: glpat-xxxxxxxxxxxxExecuting part of a playbook by tag
To execute only part of the playbook, set tags in the tags and skipTags fields. They work the same way as the --tags and --skip-tags parameters of the Ansible command line. An example of a run that executes only part of the playbook:
playbook:
type: Git
git:
url: https://github.com/example-org/infra
path: site.yml
tags: [deploy, config] # Execute only the tasks with these tags.
skipTags: [migrations] # Leave out the tasks with these tags.The settings above correspond to the following command:
ansible-playbook -i inventory.yml site.yml --tags deploy,config --skip-tags migrationsIf both fields are set, the run executes the tasks selected by tags minus those excluded by skipTags, the same way as the ansible-playbook utility.
A tag name can be any string without whitespace or commas. The comma separates tags in the command-line argument, and Ansible strips whitespace itself. Other notations used in playbooks are allowed, for example role:install and db/migrate.
The tags reserved by Ansible keep their standard meaning, which is listed in the table below.
| Tag | Meaning |
|---|---|
always |
Executed under any filter, unless named in skipTags |
never |
Executed only when named in tags |
tagged |
Every task that carries a tag |
untagged |
Every task that carries none |
all |
Every task. Used by default |
Filter with no matches
A tag filter that matches no task is not an error. The ansible-playbook utility exits successfully without executing a single task, and the run ends in the PlaybookSucceeded phase with zero counters. A typo in a tag name produces the same result, so the run reports this case in its status:
$ d8 k get ansiblerun configure-db -n demo -o jsonpath='{.status.message}'
No task matched the tags of the run.
Runner options
To check a playbook without changes on the hosts or to see detailed output, use the runner block. It changes how the playbook is executed and how much it prints, but does not affect which tasks are executed:
runner:
dryRun: true # Ansible check mode.
diff: true
verbosity: 2 # From 0 to 4.The settings above correspond to the following command:
ansible-playbook -i inventory.yml site.yml --check --diff -vvWith the dryRun parameter the run reports which changes would be made, and with the diff parameter it also prints the changes themselves. In check mode Ansible skips the tasks of the command and shell modules.
If the connection Secret contains an ansible-vault-password key, DP passes the --vault-password-file parameter to the ansible-playbook utility, so a project with files encrypted by Ansible Vault is executed unchanged.
Variables
Variables set the values a playbook is executed with, so the playbook itself does not have to change. Thanks to that, one Git project serves several environments, and a task receives a password that is not stored in the run manifest.
In the simplest case a variable holds a value the playbook reads as {{ app_version }}:
spec:
playbook:
vars:
- name: app_version
value: "1.4.2"The notation matches the env block of a Pod. The value of a variable is either set directly in the value field or taken from another object through the valueFrom field. The value field accepts a string, a number, a list or a mapping.
A value can be supplied in three ways, which are listed in the table below.
| Way | When to use |
|---|---|
vars with the value field |
The value relates to this run and is set directly in the manifest |
vars with the valueFrom field |
The value is already stored in one key of a Secret or a ConfigMap |
varsFiles |
A whole set of values with structure and types has to be passed |
The manifest below uses all three ways:
spec:
playbook:
vars:
- name: app_version
value: "1.4.2"
- name: packages
value: [nginx, curl, git]
- name: users
value:
- name: deploy
groups: [wheel]
- name: db_password
valueFrom:
secretKeyRef:
name: app-secrets
key: db-password
varsFiles:
- configMapRef:
name: app-configThe settings above correspond to the following command:
ansible-playbook -i inventory.yml site.yml \
-e @app-config-vars.yml \
-e '{"app_version":"1.4.2","packages":["nginx","curl","git"],"users":[{"name":"deploy","groups":["wheel"]}]}' \
-e db_password="$DB_PASSWORD"The spec.playbook.vars field corresponds to the Extra Variables field of AWX and to the -e parameter of the command line and has the same highest precedence. The full order of precedence is given in Precedence of values.
A variable name has to be an Ansible identifier. The naming rules and the list of refused names are given in Refused variable names.
Values from a Secret or a ConfigMap
The valueFrom field takes the value of a variable from one key of a Secret or a ConfigMap, so you do not have to copy a password or another stored value into the run manifest. The playbook sees the variable under the name set in the run:
spec:
playbook:
vars:
- name: db_password
valueFrom:
secretKeyRef:
name: app-secrets
key: db-passwordThe kubelet substitutes the value when the runner Pod starts. DP reads the Secret and the ConfigMap only to check the key names, so the value reaches no object of the module and does not appear in the process arguments that ps shows. The variables file holds only a reference to the environment variable:
db_password: "{{ lookup('env', 'ANSIBLE_VAR_db_password') }}"This way of passing values has the following properties:
- Permissions on the Secret: Kubernetes does not check them when the Pod starts, so the right to create runs lets a user read any Secret or ConfigMap in the namespace. Details are in Permissions and security.
- Name of the environment variable: The
ANSIBLE_VAR_prefix followed by the variable name, case preserved. Ansible treatsdb_passwordandDB_PASSWORDas different variables, and the environment of the Pod holds different entries for them as well. - One name, one value: If one variable takes its values from two different keys (for example, two hosts name different Secrets), the run fails with the
InvalidVariablesreason, because the runner Pod has a single environment. - A key holding binary data (
binaryDataof a ConfigMap): It never becomes a variable. If such a key is named explicitly, the run fails with a message that says so.
The playbook always receives a value from a single key as a string, even if the key holds a list. Pass a set of values with structure as a file. How to parse the string in the playbook is described in Type of a value from a single key.
Set of variables in a file
A set of variables can be described in a file, the same way as in the group_vars/all.yml file of an ordinary Ansible project. Place such a file in a ConfigMap or a Secret resource and name it in the run. This is equivalent to the -e @vars.yml parameter of the command line. An example of a ConfigMap with a set of variables:
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
vars.yaml: | # An ordinary YAML document.
app_tier: backend
app_version: "1.10" # Quoted, so it stays a string.
packages: [nginx, curl] # A list stays a list.
limits:
cpu: 2
mem: 4GiAn example of a run that uses two variables files:
spec:
playbook:
varsFiles:
- configMapRef:
name: app-config # The key defaults to vars.yaml.
- secretRef:
name: app-secrets
key: prod.yaml
optional: trueIf the key field is not set, DP uses the vars.yaml key. Set the key field if a single object contains several sets of variables, such as dev.yaml and prod.yaml.
The main advantage of a file is that it keeps the types of values. The author of the file states what is a string and what is a number, while a single key of a Secret or a ConfigMap cannot carry this information. For example, the value 1.10 in a single key stays text, and turning it into a number would change a version into 1.1.
The kubelet mounts the key into the runner Pod, and the ansible-playbook utility reads the file, not DP. The values from the file therefore show up neither in the manifest of the run nor in the process list on the node. For the same reason DP does not check the variable names inside the file. Why this matters is described in Permissions and security.
Host variables
Host variables describe the host itself rather than the run, so they are set next to the host they belong to.
The hosts[].vars field is declared the same way as a variable of a run, valueFrom sources included. An example:
target:
type: Hosts
hosts:
- address: 192.168.55.10
vars:
- name: app_role
value: frontend
- name: ansible_port
value: 2222
groups: [web]Such variables land in the inventory file next to the host they describe and are meant for information about it, for example datacenter, app_role or a non-standard SSH port. For a virtual machine (VM), annotations set the same values.
A variable of the same name in the spec.playbook.vars field takes precedence over both the annotation and the hosts[].vars field, because DP passes this field as --extra-vars.
The exception is the case where both values come through valueFrom from different keys of a Secret or a ConfigMap. There is a single environment per runner Pod, so precedence does not apply in this case, and the run fails with the InvalidVariables reason before the playbook starts. To avoid that, give such variables different names. This restriction does not affect two values from the value field, or a value from value together with a value from valueFrom.
Missing source of variables
If a source of variables is missing, the behaviour of the run depends on what is missing (the object or a key in it) and on whether the optional parameter is set. The possible cases are listed in the table below.
| Situation | Behaviour of the run |
|---|---|
The object is missing, no optional |
The run stays in the Pending phase with the DependenciesReady=False condition and the VarsSourceNotFound reason, and proceeds once the object appears. No VM is reserved while the run waits |
The object is missing, optional: true |
The run proceeds without those variables |
The object is there, a key named by valueFrom is missing |
The run fails with DependencyInvalid |
The object is there, the key is missing, optional: true on the selector |
The variable is simply absent |
For a missing key with optional: true, DP does not create the variable, so the when: x is defined construct and the default() filter work in the playbook.
Only variable sources have the optional field. The connection.secretRef and playbook.configMapRef fields do not have it, because a run without credentials or without a playbook cannot be executed.
Precedence of values
The variables of a run take precedence over every value a playbook or its project sets. A host variable comes from an annotation of a VM or from the hosts[].vars field. It takes precedence over the group_vars/ directory of the project but loses to a vars block inside a play.
Every value from the vars and varsFiles fields is passed as --extra-vars, that is level 22 of the Ansible precedence list, the highest one. The order of precedence, from the lowest to the highest, is listed in the table below.
| Where the value comes from | Ansible level |
|---|---|
| Role defaults | 2 |
The project’s group_vars/all.yml |
5 |
Host variables: VM annotations, hosts[].vars |
8 |
vars: in the play |
12 |
vars_files |
14 |
| Role vars | 15 |
set_fact |
19 |
spec.playbook.varsFiles, in list order |
22 |
spec.playbook.vars |
22 |
A play in the table is a - hosts: … tasks: … block, and a playbook may hold several of them.
The spec.playbook.vars and spec.playbook.varsFiles fields sit on the same level of precedence, and the order of the arguments decides between them. The files are passed first, in list order, and the spec.playbook.vars field is passed last. The ansible-playbook utility applies -e parameters from left to right, and the entry further right wins. The resulting order is visible in the arguments of the runner Pod:
d8 k -n <NAMESPACE> get pod -l ansible.deckhouse.io/owner=<RUN_NAME> \
-o jsonpath='{.spec.containers[0].args}'
# … -e @/ansible/vars.d/0/vars.yaml -e @/ansible/vars.d/1/prod.yaml -e @/ansible/vars.ymlHere <NAMESPACE> is the namespace of the run, and <RUN_NAME> is the name of the run.
A value with a higher precedence replaces the variable as a whole, and mappings do not merge. For example, if one file sets limits: {cpu: 2, mem: 4Gi} and the next one sets limits: {cpu: 4}, the playbook receives limits: {cpu: 4}, and the mem value is lost. Keep a whole mapping in one file, and override individual variables with the spec.playbook.vars field, whose value no file overrides.
It follows that a shared Git project cannot protect its variables from being overridden by a run. Keep values that relate to the project in its group_vars/ directory, and use the vars field for values that differ between runs.
Resolving a name conflict
In the example below, the playbook sets the app_version variable, the VM has an annotation with the same name, and the run passes its own value:
# VirtualMachine
metadata:
annotations:
vars.ansible.deckhouse.io/app_version: "from-annotation" # Level 8.
---
# AnsibleRun
spec:
playbook:
type: Inline
inline: |
- hosts: all
vars:
app_version: "from-the-playbook" # Level 12.
tasks:
- ansible.builtin.debug:
msg: "{{ app_version }}"
vars:
- name: app_version
value: "from-the-run" # Level 22.The task prints the value from-the-run. Without the spec.playbook.vars field, the task prints the value from-the-playbook rather than the value of the annotation, because level 12 is higher than level 8. The value of an annotation applies only when the playbook does not set that variable.
Types of values
The way a value is written in the manifest determines its type. The resource schema checks variable names but not their values, so a typo inside a nested structure shows up only when the playbook is executed.
Keep in mind the YAML pitfall where the value 1.10 is read as the number 1.1 and the value no as the boolean false. Quote versions so that they stay strings:
vars:
- name: app_version
value: "1.10" # Unquoted, it becomes 1.1.Integers keep their type, so the value value: 1000000 reaches the playbook as the integer 1,000,000 without losing precision.
Type of a value from a single key
A key of a Secret or a ConfigMap holds a string, and the playbook receives exactly that string. The value field and a document from the varsFiles field keep the type of a value, while a single key does not. An example:
vars:
- name: packages
value: [nginx, curl] # The playbook receives a list.
- name: extra_packages
valueFrom:
configMapKeyRef: # The playbook receives the string "[nginx, curl]".
name: app-config
key: extra_packagesIf the playbook needs a list, parse the string in the playbook itself:
- hosts: all
tasks:
- ansible.builtin.package:
name: "{{ extra_packages | from_yaml }}"
state: presentThis is why a task fails with an error such as “expected a list, got a string”. Pass a set of values with structure as a document in the varsFiles field. The document already sets the types of the values, so no string has to be parsed.
Value with curly braces
Whether a Jinja expression inside a value is evaluated depends on the source of that value. The possible cases are listed in the table below.
| Written as | What the playbook receives |
|---|---|
value: "{{ 7 * 6 }}" in the manifest |
42 |
| The same text in a Secret or a ConfigMap | {{ 7 * 6 }}, never evaluated |
The author of the run writes the value field, so Ansible templates its value the same way as a variable in any variables file. A value from the valueFrom field arrives through the lookup('env', ...) expression, and Ansible marks the result of such an expression as unsafe. Because of that, the contents of an object the run does not own cannot become an expression executed inside the runner Pod.
To keep braces literal in the value field, escape them the way Ansible expects:
vars:
- name: template_source
value: "{{ '{{' }} not a template {{ '}}' }}" # The playbook receives {{ not a template }}.Refused variable names
A variable name has to be an Ansible identifier that matches the [A-Za-z_][A-Za-z0-9_]* pattern. For example, Ansible treats the {{ app-role }} expression not as a variable reference but as a subtraction. The API server rejects a name that does not match the pattern, and the d8 k apply command returns an error with the field name right away:
The AnsibleRun "pattern-check" is invalid:
* spec.target.hosts[0].vars[0].name: Invalid value: "app-role": spec.target.hosts[0].vars[0].name in body should match '^[A-Za-z_][A-Za-z0-9_]*$'Group names follow the same rule and are rejected the same way.
In addition, two groups of names are refused.
The first group includes the names whose values DP sets itself, for example ansible_host with the address of a VM. Such a name cannot be used in any field where a variable is declared, because the API has a separate field for each of them. Two sources of one value lead to authentication errors that are hard to diagnose. The table below maps the names to the fields.
| Variable | Field that sets the value |
|---|---|
ansible_host |
hosts[].address, or the status.ipAddress of the VM |
ansible_user |
The username key of the connection Secret |
ansible_password, ansible_ssh_pass |
The password key |
ansible_ssh_private_key_file |
The ssh-privatekey key |
ansible_become_password, ansible_become_pass |
The become-password key |
ansible_connection |
spec.connection.type |
The ansible_connection variable with the local value would execute the playbook inside the runner Pod, and the run would report success, although not a single task was executed on the target VMs.
Every other variable with the ansible_ prefix is allowed in the run manifest, for example ansible_port, ansible_python_interpreter, ansible_become_user, ansible_become_method, ansible_shell_type and ansible_ssh_common_args.
The second group includes Ansible special variables (magic variables), for example groups, hostvars, inventory_hostname, playbook_dir, role_path and omit. They cannot be used in any field of a run. Ansible fills the values of such variables itself, so setting one either has no effect or breaks the evaluation of expressions. The omit variable is especially dangerous, because its value means “leave this parameter out”. A variable with that name would silently change what every task using omit does.
The table below shows where and how names are checked.
| Where the name comes from | Does not match the pattern | One of the two groups above |
|---|---|---|
spec.playbook.vars, hosts[].vars |
The d8 k apply command returns an error |
The run fails with the InvalidVariables reason before DP creates the runner Pod |
A varsFiles document |
Not checked, because DP does not read the file | Not checked |
| A VM annotation | The run fails with the InvalidInventoryAnnotations reason |
The run fails with the same reason. In addition, every variable with the ansible_ prefix except ansible_port is refused |
If a name belongs to one of the refused groups, the run ends before DP creates the runner Pod. The status message names the field and the reason:
status:
phase: Error
message: 'The variable "omit" in spec.playbook.vars[0] cannot be used: it shadows an Ansible magic variable, which Ansible fills in itself.'
conditions:
- type: Completed
status: "False"
reason: InvalidVariablesIf DP sets the value of such a variable itself, the message states where DP takes it from:
The variable "ansible_user" in spec.playbook.vars[0] cannot be used: the module sets it itself, from the username key of the connection Secret.Scheduled runs
To execute a playbook regularly, create an AnsibleRunSchedule resource. It works the same way as a CronJob resource and creates a separate run with a status of its own each time the schedule triggers. An example of a schedule:
apiVersion: ansible.deckhouse.io/v1alpha1
kind: AnsibleRunSchedule
metadata:
name: nightly-hardening
namespace: demo
spec:
schedule: "0 4 * * *"
timeZone: "Europe/Moscow" # UTC by default.
concurrencyPolicy: Forbid # Forbid (default) | Allow | Replace
successfulRunsHistoryLimit: 3
failedRunsHistoryLimit: 1
startingDeadlineSeconds: 300 # A run delayed by more than this past its scheduled time is skipped.
suspend: false
template: # The template of the run created each time the schedule triggers.
spec:
target: { type: VirtualMachines, virtualMachines: { selector: { matchLabels: { role: worker } } } }
connection: { secretRef: { name: ssh-creds } }
playbook: { type: Git, git: { url: https://github.com/example-org/infra, path: playbooks/site.yml } }A schedule works by the following rules:
- A run is named after its schedule plus the scheduled time in Unix time format, for example
nightly-hardening-1735660800. Old runs are pruned by the history limits, and deleting a schedule deletes all of its runs. - The schedule spec, run template included, can be changed. Changes apply from the next scheduled time, while a run already started continues with the previous template.
- With
concurrencyPolicy: Forbid, DP skips a scheduled time while the previous run has not finished. Skipped runs are never replayed. - Setting
suspend: truepauses the creation of new runs, while the runs already created continue to completion. - If the schedule or the time zone is invalid, the
Readycondition has theInvalidSchedulereason, and DP creates no runs until the error is fixed.
Run results
A run records in its own status the phase of the execution, the per-host task counters and the list of failed tasks.
The following commands show the result:
d8 k get ansibleruns -n demo
d8 k get ansiblerun configure-vm -n demo -o yamlAn example of the status of a successful run:
status:
phase: PlaybookSucceeded
message: The playbook run completed successfully.
hosts: # One entry per host, as in PLAY RECAP.
- name: demo-vm-01
address: 10.66.10.2
ok: 3
changed: 1
rescued: 1 # Errors handled by a rescue block.
ignored: 0 # Errors skipped by ignore_errors.
summary: { total: 1, successful: 1, failed: 0, unreachable: 0, skipped: 0 }
failures: [] # Failed tasks with their output, up to 20 entries.
podRef: { name: d8a5e-configure-vm-x7k2p, namespace: demo }
playbookCommit: 82b6d90a… # Git source only.
conditions:
- type: Completed
status: "True"
reason: PlaybookSucceededA run can be in one of the phases listed in the table below.
| Phase | Meaning |
|---|---|
Pending |
DP checks the dependencies and the targets of the run |
Running |
The runner Pod is created. The image is pulled, the init containers are executed, then the playbook |
PlaybookSucceeded |
The playbook was executed successfully, and DP deleted the runner Pod |
PlaybookFailed |
The playbook was executed and reported failed tasks or unreachable hosts. A run with the RunFailed reason gets the same phase, although its playbook was never executed |
Error |
The execution of the playbook never started, so there is no Ansible output |
The terminal phase tells you where to look for the cause of an error. With PlaybookFailed the playbook was executed, so look for the cause in its tasks. The exception is the RunFailed reason, where the playbook never started. With Error the run never reached the playbook, so look for the cause in the run setup, for example in dependencies, targets or the network.
Errors a playbook handles itself, through a rescue block or the ignore_errors parameter, are counted in rescued and ignored. The playbook is still considered successful, and the status.failures list stays empty. Ansible still prints such errors with the fatal: prefix in the log, so track them by the rescued and ignored counters.
Troubleshooting
Start troubleshooting a run with the phase of the execution and the reason in the Completed condition. The details are in the status.message field, and the status.podRef.name field holds the name of the runner Pod whose logs contain the full output.
The reasons why a run ends or waits in the Pending phase are listed in the table below. The reasons why individual virtual machines (VMs) are skipped follow after it.
| Reason | Phase | Description | Action |
|---|---|---|---|
PlaybookConfigMapNotFound |
Pending |
The ConfigMap with the playbook is not found. The reason is given in the DependenciesReady condition |
Create the ConfigMap. The run proceeds once the object appears |
VarsSourceNotFound |
Pending |
A Secret or ConfigMap with variables is not found. The reason is given in the DependenciesReady condition |
Create the object or mark the source with optional: true, as described in Missing source of variables |
DependencyMissing / DependencyInvalid |
Error |
DP does not find a Secret or ConfigMap that the run references, or the object does not contain the named key. For example, these are the connection credentials, the Git credentials or the valueFrom key of a variable. The same reason is set if the run uses an additional network and the sdn module is not enabled in the cluster |
Create the missing object or key. Its name is given in the status message |
PlaybookConfigMapInvalid |
Error |
The ConfigMap with the playbook does not contain the named key | Add the key to the ConfigMap |
InvalidSelector |
Error |
The target selector is empty or malformed | State the labels or the VM names explicitly |
InvalidInventoryAnnotations |
Error |
A variable or group name in a VM annotation or in the hosts[].groups field is not allowed |
Fix the name given in the status message |
InvalidVariables |
Error |
A variable in vars or hosts[].vars cannot be used. For example, the name is refused or the entry sets both value and valueFrom or neither. The same reason is set if the source object has an empty name or two values map to one environment variable |
Fix the variable given in the status message. The rules are described in Refused variable names |
EditionNotSupported |
Error |
The run uses targets of type Hosts or an additional network, and the cluster runs DP Open |
Use targets of type VirtualMachines in the main network |
NoEligibleTargets |
Error |
No target of the run is eligible for execution | Check the skip reason of every target in the status.skippedHosts list |
NetworkNotFound |
Error |
A Network or ClusterNetwork with the given name is not found | Check the name and the type of the network. A project network is visible only in its own namespace |
NetworkWithoutIPAM |
Error |
The network has no address pool | Bind an address pool to the network. If the hosts are reachable from the main network, specify them as targets of type Hosts without the network block |
GitCloneFailed |
Error |
The checkout of the repository failed | Read the logs with d8 k logs <POD_NAME> -c git-clone |
GalaxyInstallFailed |
Error |
The installation of Galaxy content failed | Read the logs with d8 k logs <POD_NAME> -c galaxy-install |
PlaybookFailed |
PlaybookFailed |
Tasks failed or hosts were unreachable | Read the status.failures list, then the Pod logs |
AnsibleRunnerPodFailed |
PlaybookFailed |
The runner Pod terminated without a result that can be parsed | Read the Pod logs. The Pod is kept in this case |
RunFailed |
PlaybookFailed |
DP could not prepare the run, for example it could not read a VM or build the inventory. DP does not create the runner Pod in this case, although the run gets the PlaybookFailed phase |
Read status.message. If the error is temporary, create the run again |
Here <POD_NAME> is the name of the runner Pod from the status.podRef.name field.
The VMs a run skipped are listed in status.skippedHosts. The skip reasons are listed in the table below.
Reason in skippedHosts |
Description |
|---|---|
TargetNotReady / TargetNotRunning |
The VM is starting (Pending, Starting) or is not running (Stopping, Stopped, Terminating, Failed) |
NoAddress |
The VM has no address in the main network yet |
NetworkNotAttached |
The network named in the run is not attached to this VM |
NoAddressInNetwork |
The network is attached, but no address in it yet |
TargetNotFound |
A name from names does not exist |
TargetBusy |
Another active run is using the VM, its name is given in the busyBy field |
Some problems have no reason of their own in the status of a run. Such symptoms and the way to investigate them are listed in the table below.
| Symptom | How to investigate |
|---|---|
| A task receives a string where a list or a number was expected | The value came through valueFrom, and a single key holds a string, as described in Type of a value from a single key. Pass a set of values with structure as a file |
| A variable set in the run has no effect | The same name is set by another value at precedence level 22, or the playbook reads a different name. Check the precedence of values |
Hosts in the unreachable state |
Check the SSH connection: the user and the key or password in the Secret, and whether the guest OS answers at that address. In an additional network, check whether the DHCP client configured the interface |
The run stays in the Pending phase for a long time |
The DependenciesReady condition names the object the run is waiting for |
The runner Pod stays in ContainerCreating |
The network of the run could not be configured. The reason reported by the sdn module is shown by the d8 k describe pod command |
The following command shows the logs of the runner Pod:
d8 k logs -n demo -l ansible.deckhouse.io/owner=configure-vmIf the name of a run is longer than 63 characters, DP shortens the value of the ansible.deckhouse.io/owner label and appends a hash to it. A selector with the full run name then does not find such a Pod. In this case, take the Pod name from the status.podRef.name field.
The status.failures list holds up to 20 task errors, while the complete output stays in the logs of the Pod. A failed run keeps its Pod until the AnsibleRun object is deleted, but DP deletes the Pod of a successful run right after the run finishes, and the full Ansible output disappears with it. To keep that output, configure log collection into log storage. In a playbook with several tasks, the task field may point to a wrong task, so check it against the Pod logs.
Collecting logs into log storage
To keep the complete output of the ansible-playbook utility available after the runner Pod is deleted, configure its collection with the log-shipper module. The output stays only in the logs of the Pod, and DP deletes the Pod of a successful run right after the run finishes.
The ready-made DP log collection configurations cover only namespaces labeled heritage: deckhouse, while runner Pods are created in the namespace of the run, so collecting the logs of runs requires a configuration of your own.
The collector reads the log file to its end even after the Pod is gone. The log of a run that finishes in seconds therefore reaches log storage whole, the PLAY RECAP line included. The tail of the output can be lost only if the log destination is unavailable during exactly those seconds.
Collection configured in a project
The owner of a project can collect the logs of runs in its namespace with a PodLoggingConfig resource, because this resource is namespaced. The configuration selects Pods by the label that DP puts on every runner Pod. The name of a Pod is not suitable for selection, because the API server appends a random suffix to it, for example d8a5e-configure-vm-x7k2p. An example of a configuration that sends the logs of every run in the namespace to Loki:
apiVersion: deckhouse.io/v1alpha1
kind: PodLoggingConfig
metadata:
name: ansible-runners
namespace: demo
spec:
labelSelector:
matchLabels:
app.kubernetes.io/managed-by: ansible-controller
clusterDestinationRefs:
- d8-lokiThe clusterDestinationRefs field points at a log destination that already exists in the cluster. The available destinations can be listed with the following command:
d8 k get clusterlogdestinationsCollection configured cluster-wide
The destination (ClusterLogDestination) and log collection across every project (ClusterLoggingConfig) resources are cluster-scoped, so only a cluster administrator can create them. The upside is that the configuration is set up once for the whole cluster, and projects need no configurations of their own:
apiVersion: deckhouse.io/v1alpha2
kind: ClusterLoggingConfig
metadata:
name: ansible-runner-logs
spec:
type: KubernetesPods
kubernetesPods:
labelSelector:
matchLabels:
app.kubernetes.io/managed-by: ansible-controller
destinationRefs:
- d8-lokiThe Loki that ships with DP is meant for looking into logs right away rather than for archiving them. The retentionPeriodHours parameter of the loki module sets its retention period. But Loki also frees space as the disk fills up, so logs may be kept for less than that period.
To keep the logs of runs for months, create a separate ClusterLogDestination resource for external log storage. The log-shipper module supports the Loki, Elasticsearch, Logstash, Vector, Kafka, Splunk and Socket destinations. The same collection can feed several destinations at once if you list them in the destinationRefs field.
Searching for the log of a run in Loki
Once log collection is configured, the output of a run lands in a stream labeled with the namespace, the Pod and the container. The container with the playbook output is named ansible, and the name of the runner Pod starts with d8a5e- followed by the name of the run. If the name of the run is longer than 51 characters, the Pod name contains only its beginning. The LogQL queries below cover the common cases:
# Every run in a namespace.
{namespace="demo", container="ansible"}
# A single run.
{namespace="demo", pod=~"d8a5e-configure-vm-.+"}
# Every run created by one schedule.
{namespace="demo", pod=~"d8a5e-nightly-hardening-.+"}
# Recap only. The PLAY RECAP line holds ok, changed and failed per host.
{namespace="demo", container="ansible"} |= "PLAY RECAP"
# Failed tasks and unreachable hosts.
{namespace="demo", container="ansible"} |~ "fatal:|UNREACHABLE"
# Number of task errors per Pod over a day.
sum by (pod) (count_over_time({container="ansible"} |= "fatal:" [24h]))Permissions and security
This section is meant for a cluster administrator who grants users the right to create runs. This right gives wider access to data and networks than it may seem, so take the properties below into account before you grant it.
Reading Secrets and ConfigMaps of a namespace
Kubernetes does not check permissions on a Secret when a Pod references it, and the runner Pod is no exception. The author of a run writes the playbook and can name any Secret or ConfigMap of the namespace in the valueFrom field, so the right to create runs is enough to read their contents. Keep that in mind when granting this right in a namespace whose Secrets not everyone should read.
Additional networks
A ClusterNetwork is visible from any namespace, so the right to create an AnsibleRun also means the right to attach a runner Pod to any cluster-wide L2 domain. The sdn module behaves this way for every workload, not only for runs.
A NetworkPolicy of the project constrains the pod network only, so it does not limit what a run does inside a VLAN.
Variables files
The kubelet mounts the document from the varsFiles field into the runner Pod, and the ansible-playbook utility reads the file, not DP. DP therefore does not check the variable names inside the file, and refused names take effect from it. The ansible_connection variable with the local value is especially dangerous, because with it the playbook is executed inside the runner Pod and succeeds without touching a single target host.
The missing check does not extend the rights of the run author, because a variables file has the same trust level as the playbook. The author of a run already chooses the playbook, the host addresses and the connection Secret.
Annotations of virtual machines
The owner of a virtual machine (VM) can change its annotations, and that owner is not necessarily the author of the run. An annotation therefore may not set variables with the ansible_ prefix. For example, an ansible_host variable in an annotation would redirect someone else’s run to another VM and give that VM the SSH credentials, while an ansible_ssh_common_args variable with a ProxyCommand value would execute an arbitrary command inside the runner Pod.
The only exception is ansible_port, because it does not reduce the security of a run. The host address stays the one DP resolved, so the connection reaches the same VM, and its owner already decides which service listens on each of its ports.
Numeric limits
The numeric limits of a single run are listed in the table below.
| Limit | Value |
|---|---|
| Inline playbook | 65,536 characters |
| Hosts specified by address | 1000 per run. 64 variables and 32 groups per host |
| Tags | 64 entries each in spec.playbook.tags and spec.playbook.skipTags |
| Variables | 64 entries in spec.playbook.vars. 8 files in spec.playbook.varsFiles |
status.failures |
Up to 20 task errors, the remaining errors stay in the Pod logs |
| Additional networks | IPv4 only |
| AnsibleRunSchedule name | Up to 39 characters |