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 page collects ready-made scenarios for the ansible module in Deckhouse Platform (DP). The scenarios are independent, and each one includes a manifest and a description of what appears in the status of the run after it ends. The user guide describes every field of a run. Here, the runner Pod is a temporary Pod where DP executes the ansible-playbook utility.

Before you start

Every example expects the demo namespace, two virtual machines (VMs) demo-vm-01 and demo-vm-02 labeled role: demo, and the ssh-creds Secret with SSH credentials. Prepare those objects before you try the examples.

Preparing a VM and a Secret…

DP does not configure SSH inside a guest OS, so create a user with sudo rights and pass the public key of that user to the VM with a cloud-init script. The following VM manifest contains such a script. Create both VMs from it, changing the metadata.name value to demo-vm-02 for the second one:

apiVersion: virtualization.deckhouse.io/v1alpha2
kind: VirtualMachine
metadata:
  name: demo-vm-01
  namespace: demo
  labels:
    role: demo
spec:
  # Set resources, image and disk from a template of your own.
  provisioning:
    type: UserData
    userData: |
      #cloud-config
      packages:
        - openssh-server
        - qemu-guest-agent
      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 ssh
        - systemctl enable --now qemu-guest-agent

Here, <SSH_PUBLIC_KEY> is the public key of the pair whose private key you put into the Secret.

Wait for both VMs to reach the Running phase and get addresses. The PHASE and IPADDRESS columns show the phase and the address:

d8 k get vm -n demo -l role=demo

Create a Secret with SSH credentials in the same namespace. A run connects to the VMs with a private key or a password, and the commands below create a Secret for each option:

d8 k create secret generic ssh-creds -n demo \
  --from-literal=username=ansible \
  --from-file=ssh-privatekey=./id_ed25519

d8 k create secret generic ssh-creds-password -n demo \
  --from-literal=username=ansible \
  --from-literal=password=ansible

Connectivity check with an inline playbook

The simplest scenario executes a single-task playbook on the virtual machines (VMs) with a given label and goes through every step of a run, from the creation of the object to the result in its status. It is a good starting point for getting to know the module.

The manifest and the result…

The run selects the VMs by the role: demo label and executes the ping module on them:

apiVersion: ansible.deckhouse.io/v1alpha1
kind: AnsibleRun
metadata:
  name: inline-ping
  namespace: demo
spec:
  target:
    type: VirtualMachines
    virtualMachines:
      selector:
        matchLabels:
          role: demo
  connection:
    secretRef:
      name: ssh-creds
  playbook:
    type: Inline
    inline: |
      ---
      - name: Connectivity check
        hosts: all
        tasks:
          - name: Ping
            ansible.builtin.ping:

The run ends in the PlaybookSucceeded phase, and the status.hosts list contains an entry for each VM with its name, address and the ok: 1 value. Check the result with the following commands:

d8 k get ansiblerun inline-ping -n demo
d8 k get ansiblerun inline-ping -n demo -o jsonpath='{.status.hosts}' | jq

Playbook from a ConfigMap

A playbook longer than a few dozen lines, or one that several runs use, is better kept in a ConfigMap resource. The run then refers to a key of that ConfigMap and does not contain the playbook text.

The manifest and the result…

An example of a ConfigMap with a playbook and of a run that refers to that ConfigMap by name:

apiVersion: v1
kind: ConfigMap
metadata:
  name: hello-playbook
  namespace: demo
data:
  playbook.yaml: |
    ---
    - name: Hello from a ConfigMap
      hosts: all
      tasks:
        - name: Print hostname
          ansible.builtin.debug:
            msg: "Hello from {{ inventory_hostname }} ({{ ansible_hostname }})"
---
apiVersion: ansible.deckhouse.io/v1alpha1
kind: AnsibleRun
metadata:
  name: hello-from-configmap
  namespace: demo
spec:
  target:
    type: VirtualMachines
    virtualMachines:
      selector:
        matchLabels:
          role: demo
  connection:
    secretRef:
      name: ssh-creds
  playbook:
    type: ConfigMap
    configMapRef:
      name: hello-playbook
      # The key defaults to playbook.yaml.

The logs of the runner Pod contain a TASK [Print hostname] line with the name of the virtual machine (VM). If the ConfigMap resource does not exist yet, the run waits for it in the Pending phase and does not fail.

Selecting several targets

A run selects virtual machines (VMs) with a label selector or with a list of names, and the example below shows both ways. The rules of target selection are described in the Virtual machines section of the user guide.

Both ways…

The selector in the example below selects the VMs whose role label is in the list, so both prepared VMs match it:

# ...
spec:
  target:
    type: VirtualMachines
    virtualMachines:
      selector:
        matchExpressions:
          - key: role
            operator: In
            values: [demo, web]
  # ...

A list of names holds the metadata.name values of VMs from the namespace of the run:

# ...
spec:
  target:
    type: VirtualMachines
    virtualMachines:
      names: [demo-vm-01, demo-vm-02]
  # ...

VMs that DP cannot configure when the run starts go to the status.skippedHosts list, and the playbook is executed on the remaining VMs.

Debugging a run

The runner block helps you find out what a playbook does. It changes how the ansible-playbook utility executes the playbook, not which tasks it executes. The diff parameter shows what exactly changed in files, verbosity sets the level of detail in the log, and dryRun starts a check without changes on the hosts.

The manifest and what appears in the logs…

The run writes the /etc/motd file and prints the changes with a detailed log:

apiVersion: ansible.deckhouse.io/v1alpha1
kind: AnsibleRun
metadata:
  name: debug-template-with-diff
  namespace: demo
spec:
  target:
    type: VirtualMachines
    virtualMachines:
      selector:
        matchLabels:
          role: demo
  connection:
    secretRef:
      name: ssh-creds
  runner:
    diff: true
    verbosity: 2
  playbook:
    type: Inline
    inline: |
      ---
      - name: Render a file with --diff
        hosts: all
        become: true
        tasks:
          - name: Write a message of the day
            ansible.builtin.copy:
              dest: /etc/motd
              content: |
                Managed by AnsibleRun
                Host: {{ inventory_hostname }}
              mode: '0644'

With the diff: true parameter, Ansible prints the changes of the file, and with the verbosity: 2 parameter it adds details about every task. Level 4 makes the log so large that it is hard to find anything in it, so use level 4 only if level 2 is not enough.

Groups and host variables in annotations

Annotations on a virtual machine (VM) set Ansible groups and host variables. DP copies these annotations into the inventory of every run, so the playbook receives the web group and the app_role variable without changes to the run manifest.

The annotations and a run targeting a group…

Add annotations with two groups and two variables to the demo-vm-01 VM:

d8 k annotate vm demo-vm-01 -n demo \
  ansible.deckhouse.io/groups=web,production \
  vars.ansible.deckhouse.io/app_role=frontend \
  vars.ansible.deckhouse.io/datacenter=dc1

The playbook of a run uses such a group in the same way as a group from an ordinary inventory file:

# ...
spec:
  # ...
  playbook:
    type: Inline
    inline: |
      ---
      - name: Frontend rollout
        hosts: web
        tasks:
          - name: Show host variables
            ansible.builtin.debug:
              msg: "{{ app_role }} in {{ datacenter }}"

The task is executed only on demo-vm-01, because only this VM belongs to the web group. DP checks the group and variable names in annotations when a run starts, not when you save the VM. A run with an invalid name ends in the Error phase with the InvalidInventoryAnnotations reason, and the status message names the VM and the annotation. The rules for names are in the Refused variable names section of the user guide.

Installing a package with privilege escalation

Set the become: true parameter for tasks that change the system. DP takes the privilege escalation password from the become-password key of the connection Secret. The playbook in the example chooses the package manager from the facts Ansible gathered.

The manifest and the result…

The playbook updates the apt cache if the virtual machine (VM) uses that package manager, and installs the htop package:

apiVersion: ansible.deckhouse.io/v1alpha1
kind: AnsibleRun
metadata:
  name: install-htop
  namespace: demo
spec:
  target:
    type: VirtualMachines
    virtualMachines:
      selector:
        matchLabels:
          role: demo
  connection:
    secretRef:
      name: ssh-creds
  playbook:
    type: Inline
    inline: |
      ---
      - name: Install htop
        hosts: all
        become: true
        tasks:
          - name: Update the apt cache
            ansible.builtin.apt:
              update_cache: true
              cache_valid_time: 3600
            when: ansible_facts.pkg_mgr == 'apt'

          - name: Install the package
            ansible.builtin.package:
              name: htop
              state: present

The install task reports the changed state the first time and the ok state on a repeated execution, because the package is already installed. If sudo on the VM requests a password, add the password to the Secret under the become-password key.

Idempotent configuration of a service

The playbook configures the service with a config file and a handler that reloads the service only if the file changes. A repeated run with the same playbook does not change the configuration and does not reload the service.

The manifest and the result…

The run writes an nginx config and reloads the service through a handler:

apiVersion: ansible.deckhouse.io/v1alpha1
kind: AnsibleRun
metadata:
  name: render-nginx-config
  namespace: demo
spec:
  target:
    type: VirtualMachines
    virtualMachines:
      selector:
        matchLabels:
          role: demo
  connection:
    secretRef:
      name: ssh-creds
  playbook:
    type: Inline
    inline: |
      ---
      - name: Configure nginx
        hosts: all
        become: true
        tasks:
          - name: Write the site config
            ansible.builtin.copy:
              dest: /etc/nginx/conf.d/ansible-demo.conf
              mode: '0644'
              content: |
                server {
                  listen 8080 default_server;
                  server_name {{ inventory_hostname }};
                  location / {
                    return 200 "OK from {{ inventory_hostname }}\n";
                  }
                }
            notify: Reload nginx
        handlers:
          - name: Reload nginx
            ansible.builtin.service:
              name: nginx
              state: reloaded

The first run shows changed: 2 for each VM in the status.hosts list, because the config file changed and the handler was executed (the service module with state: reloaded always reports a change). The second run shows changed: 0. Keep a project with roles and templates in a Git repository, not in the inline field.

Collecting facts about VMs

For an inventory of virtual machines (VMs) and a capacity estimate, use a playbook that only gathers and prints facts and changes nothing. The result of such a playbook stays only in the logs of the runner Pod.

The manifest and the result…

The playbook gathers facts and prints the main ones:

apiVersion: ansible.deckhouse.io/v1alpha1
kind: AnsibleRun
metadata:
  name: weekly-audit
  namespace: demo
spec:
  target:
    type: VirtualMachines
    virtualMachines:
      selector:
        matchLabels:
          role: demo
  connection:
    secretRef:
      name: ssh-creds
  playbook:
    type: Inline
    inline: |
      ---
      - name: Weekly audit
        hosts: all
        gather_facts: true
        tasks:
          - name: Show the snapshot
            ansible.builtin.debug:
              msg:
                os: "{{ ansible_distribution }} {{ ansible_distribution_version }}"
                kernel: "{{ ansible_kernel }}"
                cores: "{{ ansible_processor_vcpus }}"
                mem_mb: "{{ ansible_memtotal_mb }}"
                uptime_sec: "{{ ansible_uptime_seconds }}"

The run succeeds, the changed counter stays at zero, and the gathered values go to the log. DP deletes the Pod of a successful run right after the run ends, so to keep the log, configure log collection into log storage.

Encrypted values in Ansible Vault

A project with files encrypted by Ansible Vault is executed unchanged if the password is in the ansible-vault-password key of the connection Secret. DP passes that password to the ansible-playbook utility as a password file.

The Secret and the run…

Create a connection Secret with the ansible-vault-password key:

d8 k create secret generic ssh-creds-vault -n demo \
  --from-literal=username=ansible \
  --from-file=ssh-privatekey=./id_ed25519 \
  --from-literal=ansible-vault-password='vault-pass'

Encrypt the value with the ansible-vault encrypt_string command and paste the result into the playbook unchanged:

# ...
spec:
  # ...
  connection:
    secretRef:
      name: ssh-creds-vault
  playbook:
    type: Inline
    inline: |
      ---
      - name: Use a vault-encrypted value
        hosts: all
        vars:
          api_token: !vault |
            $ANSIBLE_VAULT;1.1;AES256
            6633...6464
        tasks:
          - name: Write the token
            become: true
            ansible.builtin.copy:
              dest: /etc/example/api-token
              content: "{{ api_token }}"
              mode: '0600'

Without the ansible-vault-password key, the playbook starts but fails when Ansible decrypts the value. The status.failures list then contains the Ansible error about a missing password.

Playbook from a public Git repository

The Git source fetches the whole repository, so an Ansible project with roles/, group_vars/ directories and templates is executed unchanged. DP installs the declared Galaxy dependencies before the playbook starts.

The manifest and the result…

The run fetches a project from a public repository and executes the playbooks/site.yml playbook:

apiVersion: ansible.deckhouse.io/v1alpha1
kind: AnsibleRun
metadata:
  name: git-playbook
  namespace: demo
spec:
  target:
    type: VirtualMachines
    virtualMachines:
      selector:
        matchLabels:
          role: demo
  connection:
    secretRef:
      name: ssh-creds
  playbook:
    type: Git
    git:
      url: https://github.com/example-org/ansible-demo
      revision: v1.4.0                 # A branch, a tag or a commit SHA.
      path: playbooks/site.yml         # Defaults to playbook.yaml.

DP records the commit that was actually executed in the status.playbookCommit field. If the repository contains a requirements.yml, roles/requirements.yml or collections/requirements.yml file, DP installs the roles and collections declared in it before the playbook starts. Those steps are visible in the logs of the git-clone and galaxy-install init containers.

Private Git repository

The credentials of a private repository are kept in a separate Secret. DP passes them only to the fetch step, so the tasks of the playbook do not see them. For ssh:// addresses this Secret is mandatory and has to contain the ssh-privatekey key and also the known_hosts key, because the SSH client checks the key of the Git server strictly.

Both ways of access…

For access over SSH, put into the Secret a private key and the contents of the known_hosts file, which the ssh-keyscan command prints:

d8 k create secret generic git-ssh -n demo \
  --from-file=ssh-privatekey=./deploy_key \
  --from-file=known_hosts=<(ssh-keyscan github.com)

The run refers to that Secret in the git.secretRef field:

# ...
spec:
  # ...
  playbook:
    type: Git
    git:
      url: ssh://git@github.com/example-org/ansible-project.git
      revision: main
      path: playbooks/site.yml
      secretRef:
        name: git-ssh

For access over HTTPS, pass the token as the password. The user name depends on the service, for example GitLab accepts oauth2, or gitlab-ci-token with a job token:

d8 k create secret generic git-https -n demo \
  --from-literal=username=oauth2 \
  --from-literal=password=<TOKEN>

Here, <TOKEN> is a token allowed to read the repository. For a server with a certificate authority of its own, add a ca.crt key to the same Secret. If DP cannot fetch the repository, the run ends with the GitCloneFailed reason, and the logs of the git-clone container contain the details.

Run in an additional network

If the SSH server in a guest OS listens only in an additional sdn network, attach the run itself to that network with the connection.network field. The address of the virtual machine (VM) is not enough, because the pod network has no route into another L2 domain.

The VM, the run, and what the network needs…

In the example below, the VM uses the additional network together with the main one:

apiVersion: virtualization.deckhouse.io/v1alpha2
kind: VirtualMachine
metadata:
  name: demo-vm-vlan
  namespace: demo
  labels:
    role: vlan-demo
spec:
  networks:
    - type: Main
    - type: ClusterNetwork
      name: vlan-64

The network block of the run mirrors an entry of the spec.networks list of the VM, so copy its values from the manifest of the VM:

# ...
spec:
  target:
    type: VirtualMachines
    virtualMachines:
      selector:
        matchLabels:
          role: vlan-demo
  connection:
    secretRef:
      name: ssh-creds
    network:
      type: ClusterNetwork      # Main (the default) | Network | ClusterNetwork
      name: vlan-64
  # ...

The network must have an address pool, because both the VM and the runner Pod take an address from it. Run a DHCP client on that interface in the guest OS, otherwise DP reports an address at which the VM does not answer. The other network requirements and the error reasons are described in the Networks section of the user guide.

Troubleshooting a failed run

The playbook in this scenario fails on purpose. It shows where a run reports a failed task and where the full Ansible output is kept.

The manifest, the status and the logs…

The run executes the ansible.builtin.fail task, which always fails:

apiVersion: ansible.deckhouse.io/v1alpha1
kind: AnsibleRun
metadata:
  name: expected-failure
  namespace: demo
spec:
  target:
    type: VirtualMachines
    virtualMachines:
      selector:
        matchLabels:
          role: demo
  connection:
    secretRef:
      name: ssh-creds
  playbook:
    type: Inline
    inline: |
      ---
      - name: Failure demo
        hosts: all
        tasks:
          - name: This one fails
            ansible.builtin.fail:
              msg: Expected failure

The run ends in the PlaybookFailed phase, and the status.failures list names the host, the task and the error message:

status:
  phase: PlaybookFailed
  failures:
    - host: demo-vm-01
      task: This one fails
      message: "Expected failure"
    - host: demo-vm-02
      task: This one fails
      message: "Expected failure"

DP keeps the Pod of a failed run until you delete the AnsibleRun object, so you can read the full output in its logs:

d8 k logs -n demo -l ansible.deckhouse.io/owner=expected-failure

If a run ends in the Error phase, the playbook was never started, and there is no Ansible output. The difference between the phases is described in the Run results section of the user guide.

Scheduled run

The AnsibleRunSchedule resource creates runs on a cron schedule the way a CronJob resource creates Job objects. The name of each run contains the schedule name and the scheduled time.

The manifest and how to control it…

The schedule executes a single-task playbook every day at 04:00 Moscow time:

apiVersion: ansible.deckhouse.io/v1alpha1
kind: AnsibleRunSchedule
metadata:
  name: nightly-ping
  namespace: demo
spec:
  schedule: "0 4 * * *"
  timeZone: "Europe/Moscow"          # UTC by default.
  template:
    spec:
      target:
        type: VirtualMachines
        virtualMachines:
          selector:
            matchLabels:
              role: demo
      connection:
        secretRef:
          name: ssh-creds
      playbook:
        type: Inline
        inline: |
          ---
          - name: Nightly check
            hosts: all
            tasks:
              - name: Ping
                ansible.builtin.ping:

To select the runs that a schedule created, use the ansible.deckhouse.io/schedule-owner label:

d8 k get ansibleruns -n demo \
  -l ansible.deckhouse.io/schedule-owner=nightly-ping

You can change a schedule, including its run template, while the schedule works, and the changes apply the next time the schedule triggers. To pause the schedule, set the suspend: true field:

d8 k patch ansiblerunschedules nightly-ping -n demo \
  --type=merge -p '{"spec":{"suspend":true}}'

Concurrency and history of a schedule

Four fields of a schedule define what happens if a run does not finish before the schedule triggers again, how late a run can start and how many runs to keep in the namespace.

The manifest and what each field does…

The schedule below sets all four fields explicitly:

apiVersion: ansible.deckhouse.io/v1alpha1
kind: AnsibleRunSchedule
metadata:
  name: nightly-baseline
  namespace: demo
spec:
  schedule: "0 4 * * *"
  timeZone: "Europe/Moscow"
  concurrencyPolicy: Forbid          # Forbid (the default) | Allow | Replace
  startingDeadlineSeconds: 600
  successfulRunsHistoryLimit: 3
  failedRunsHistoryLimit: 1
  template:
    spec:
      target:
        type: VirtualMachines
        virtualMachines:
          selector:
            matchLabels:
              role: demo
      connection:
        secretRef:
          name: ssh-creds
      playbook:
        type: Inline
        inline: |
          ---
          - name: Baseline
            hosts: all
            tasks:
              - name: Ping
                ansible.builtin.ping:

With the Forbid value, the schedule skips its scheduled time while the previous run is still active. Use it for runs on the same virtual machines (VMs), because a parallel run would skip those VMs with the TargetBusy reason anyway. Use the Allow value for runs on different VMs, and the Replace value if the latest state is more important than the result of the previous attempt.

The startingDeadlineSeconds field sets how late, in seconds, a run may start. If a run is later than that, the schedule skips it and never replays it. DP deletes surplus runs by the history limits, and when you delete a schedule, DP also deletes every run that the schedule created.

Hosts specified by address

Hosts whose addresses DP does not know, for example physical servers and network appliances, are specified by address directly in the run manifest. How such targets differ from virtual machines is described in the Hosts specified by address section of the user guide.

Targets of type Hosts are available in commercial DP editions only. In DP Open, the API server rejects a manifest with such targets.

The list of hosts, their variables and groups…

The run executes a playbook against two hosts, one specified by an IP address and the other by a DNS name:

apiVersion: ansible.deckhouse.io/v1alpha1
kind: AnsibleRun
metadata:
  name: patch-appliances
  namespace: demo
spec:
  target:
    type: Hosts
    hosts:
      - address: 192.168.55.10
        groups: [web]
        vars:
          - name: app_role
            value: frontend
      - address: db.example.com
  connection:
    secretRef:
      name: ssh-creds
  playbook:
    type: Inline
    inline: |
      ---
      - name: Baseline
        hosts: all
        tasks:
          - name: Ping
            ansible.builtin.ping:

Such a host has no annotations, so its variables and groups are set directly in the list of hosts. DP writes the address of the host into the status.hosts[].name field. If the host is in an isolated network, also set the connection.network field, because DP cannot tell from the address alone which network to attach the runner Pod to.

Variables of a run

Variables pass values to a playbook, so one playbook serves several environments without changes. You can write the value of a variable directly in the manifest as a string, a number, a list or a mapping.

The manifest and the result…

The run passes a string, a list and a mapping to the playbook:

apiVersion: ansible.deckhouse.io/v1alpha1
kind: AnsibleRun
metadata:
  name: deploy-1-4-2
  namespace: demo
spec:
  target:
    type: VirtualMachines
    virtualMachines:
      selector:
        matchLabels:
          role: demo
  connection:
    secretRef:
      name: ssh-creds
  playbook:
    type: Inline
    vars:
      - name: app_version
        value: "1.4.2"                 # Quoted, so it is a string.
      - name: packages
        value: [nginx, curl]
      - name: limits
        value:
          cpu: 2
          mem: 4Gi
    inline: |
      ---
      - name: Deploy
        hosts: all
        tasks:
          - name: Show the version
            ansible.builtin.debug:
              msg: "deploying {{ app_version }}"

The log holds the deploying 1.4.2 line. Quote a version, because without quotes YAML turns 1.10 into the number 1.1. For the next rollout create a new run, because the spec block of an existing run cannot be changed.

Values from a Secret and a ConfigMap

You do not have to copy a password or a ready set of values into the manifest of a run. The valueFrom field takes the value of a variable from a key of a Secret or a ConfigMap, and the varsFiles field adds a whole YAML document with variables.

The run manifest…

The run takes a password from a key of a Secret and adds two variables files, the second of which is optional:

# ...
spec:
  # ...
  playbook:
    type: Inline
    vars:
      - name: db_password
        valueFrom:
          secretKeyRef:
            name: app-secrets
            key: db-password
    varsFiles:
      - configMapRef:
          name: app-config           # The key defaults to vars.yaml.
      - secretRef:
          name: app-secrets
          key: prod.yaml
          optional: true
    inline: |
      ---
      - name: Deploy
        hosts: all
        tasks:
          - name: Use the password
            ansible.builtin.debug:
              msg: "password length: {{ db_password | length }}"

The playbook receives a value from a single key as a string, while a file from varsFiles keeps the types of its values. What happens if the object or the key is missing is described in the Missing source of variables section of the user guide.