Ansible tips

Indent json output

The default output format when using -v[vvv] is json. However, it’s one 1 line without indentation, like that:

TASK [Set fact] ********************************************************************************************************
Tuesday 01 October 2024  15:53:10 +0200 (0:00:00.053)       0:00:00.053 *******
ok: [localhost] => {"ansible_facts": {"my_fact": "hello world"}, "changed": false}

I’ve copied this tons of time in a formatter to understand a module output.
Until… I found you can just set this in ansible.cfg:

[defaults]
callback_format_pretty = true

Result of a set_fact:

TASK [Set fact] ********************************************************************************************************
Tuesday 01 October 2024  15:53:39 +0200 (0:00:00.042)       0:00:00.042 *******
ok: [localhost] => {
    "ansible_facts": {
        "my_fact": "hello world"
    },
    "changed": false
}

That’s automatically more readable! Especially if the output json is big.

Don’t use quotes unless needed

First read this page: https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html#gotchas

# Bad
storageaccount: "artifacts"
buy_key: "abc 123"
admin_name: "contoso\\admin_svc"
chef_user: {{ ansible_user }}   # doesn't work (dictionary vs ansible var)
drive_letter: D:   # doesn't work (colons)
weird_msg: [IMPORTANT MESSAGE]    # doesn't work (array)
app_services:
  - "service A"
  - "service B"

# Good
storageaccount: artifacts
buy_key: abc with spaces
admin_name: contoso\admin_svc
chef_user: "{{ ansible_user }}"
drive_letter: "D:"
weird_msg: "[IMPORTANT MESSAGE]"
app_services:
  - service A
  - service B

More: https://blogs.perl.org/users/tinita/2018/03/strings-in-yaml---to-quote-or-not-to-quote.html

Use linters

You can use:

  • yamllint
  • ansible-lint
  • XLAB Steampunk Spotter

Harness the power of default()

time_zone: "{{ time_zones_available[customLocation | default(location) | default('UTC')] }}"

Work on more machines at a time

This is related to the default number of forks

Update the default value (currently 5) like so:

In ansible.cfg:

[defaults]
forks = 10

Make sure you have enough resources (CPU/RAM/Network) to do so.

Have a nice summary of time spent by task

In ansible.cfg:

[defaults]
callbacks_enabled = profile_tasks

Result:

Thursday 26 September 2024  14:21:05 +0000 (0:17:41.644)       0:22:41.235 **** 
=============================================================================== 
Install all updates and reboot as many times as needed --------------- 1061.64s
Install Cortex XDR ----------------------------------------------------- 76.19s
Install Qualys --------------------------------------------------------- 53.11s
Remove setup ----------------------------------------------------------- 31.11s
Collect required facts for later --------------------------------------- 26.38s
Disable disk defrag service -------------------------------------------- 23.85s
Remove setup ----------------------------------------------------------- 22.31s
Download Qualys from network share mount with a domain identity --------- 5.54s
Download Cortex XDR from network share mount with a domain identity ----- 4.34s
Get disks facts --------------------------------------------------------- 4.25s
Enable firewall for Domain, Public and Private profiles ----------------- 3.75s
Get disks volumes ------------------------------------------------------- 3.48s
Get disks facts --------------------------------------------------------- 3.45s
Firewall rule to allow ICMP v4 on all type codes ------------------------ 3.06s
Disable indexing service ------------------------------------------------ 3.01s
Ensure NetBIOS is disabled system wide ---------------------------------- 2.96s
Check edge business is installed ---------------------------------------- 2.80s
Ensure Qualys service is runnning --------------------------------------- 2.66s
Change power plan to high performance ----------------------------------- 2.64s
Remove CD/DVD drive letter ---------------------------------------------- 2.44

Improve speed [basic]

Use pipelining in ansible.cfg:

[defaults]
pipelining = true

Do not gather facts in your plays (unless needed):

---

- name: List vms with SQL Server
  hosts: windows
  gather_facts: false
  tasks:

By default, i never gather facts unless specific play.
You can also only gather specific facts with ansible.builtin.setup

Improve windows speed

Use psrp rather than winrm:

---
ansible_connection: psrp
ansible_port: 5985

Official documentation about this plugin: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/psrp_connection.html

For hosts requiring kerberos, add them to a ‘kerberos’ group and declare kerberos.yml in your group_vars:

---
ansible_connection: winrm
ansible_winrm_transport: kerberos
ansible_winrm_message_encryption: auto
ansible_winrm_kerberos_delegation: true

Simple things to troubleshoot

Show vars: ansible-inventory -i inventories/dev/ --list --limit vm1

Show facts:

- name: Show facts
  hosts: all
  gather_facts: true
  tasks:
    - name: Show facts
      ansible.builtin.debug:
        var: ansible_facts

Test connectivity:

---

- name: Test connectivity
  hosts: all
  gather_facts: false
  tasks:
    - name: Check windows connectivity
      ansible.windows.win_ping:
      when: inventory_hostname in groups['windows']
    - name: Check linux connectivity
      ansible.builtin.ping:
      when: inventory_hostname in groups['linux']

Use tag ‘always’

https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_tags.html#always-and-never

For instance, if you gather facts manually and other tasks depent on it, but the user decide to launch the playbook with –tags, it will likely skip this mandatory step and fail.
To avoid this, you can use tag ‘always’:

- name: Collect required facts for later
  ansible.builtin.setup:
    gather_subset:
      - '!all'
      - min
    filter:
      - ansible_os_installation_type
      - ansible_domain
  tags: always

Simplify ansible vault credentials

In case you are using ansible vault to store some secrets, I advise to set vault_password_file in your ansible.cfg:

[defaults]
vault_password_file = ~/.vault_pass

This will prevent any prompt and help you for ansible-vault command.

Azure dynamic inventory plugin tricks

Here is a sample:

---

plugin: azure.azcollection.azure_rm
auth_source: auto
include_vm_resource_groups:
  - "*"
subscription_id: 2c60a3c5-9d3f-40f1-973b-3ca81d281ae4
plain_host_names: true  # prevents generated suffix
default_host_filters:
  - powerstate != 'running'  # ignore stopped machine (connection will fail!)
  - provisioning_state != 'succeeded'  # ignore machine being provisioned or failed
hostvar_expressions:
  # kerberos needs FQDN ; zscaler (remote) needs dns
  ansible_host: (private_ipv4_addresses | first) if os_profile.system == 'linux' else computer_name | default(name, true)
conditional_groups:
  windows: os_profile.system == 'windows'
  linux: os_profile.system == 'linux'
  kerberos: os_profile.system == 'windows' and tags.Role is defined and tags.Role not in ['XAB', 'CBR']
keyed_groups:
  # places each host in a group named 'tag_(tag name)_(tag value)' for each tag on a VM.
  - prefix: tag
    key: tags
  - prefix: geo
    key: location

Explanations:

default_host_filters:
Ignore stopped vms or vms being built, as we cannot connect to it.
Up to the operator to know which vms remains to configure if some were stopped during playbook execution.

keyed_groups:
Will automatically create groups based on each tag and also on the location of the vm

hostvar_expressions to set ansible_host:
The default azurerm inventory plugin logic is to set ansible_host with the public or private ipv4 provided by azure. See:
https://github.com/ansible-collections/azure/blob/dev/plugins/inventory/azure_rm.py#L357 (look for ‘ansible_host’, line might change with time)
However, that isn’t great in some scenarios where fqdn is required:

  • kerberos authentication (ansible_connection: winrm ; ansible_winrm_transport: kerberos)
  • zscaler (ex: remote work)

Thus, this hostvar expression allows us to set ansible_host with infos provided by azure and use conditions.

Craft a list of objects

- name: Store result for final output
  ansible.builtin.set_fact:
    versions: |
      [
      {% for n in res.results %}
        {
          "subenv": "{{ n.item.key }}",
          "version": "{{ (n.content | b64decode | split('_'))[1] }}"
        },
      {% endfor %}
      ]      

Generate a custom report for reachable machines

- name: Merge infos  # noqa: run-once[task]
  ansible.builtin.set_fact:
    servers: |
      [
      {% for host in ansible_play_hosts_all %}
        {% set host_vars = hostvars[host] %}
        {% if host_vars.inventory_hostname not in ansible_play_hosts %} # unreachable
        {
          "name": "{{ host_vars.inventory_hostname }}",
          "ips": {{ host_vars.ansible_facts.ip_addresses }},
          "system": "{{ host_vars.os_profile.system }}",
          "os": "vm unreachable",
          "uptime_days": -1,
          "size":
          {
            "name": "{{ host_vars.virtual_machine_size | default('') }}",
            "vCPUs": 0,
            "RAM": 0,
          },
        },
        {% else %}
        {
          "name": "{{ host_vars.inventory_hostname }}",
          "ips": {{ host_vars.ansible_facts.ip_addresses }},
          "system": "{{ host_vars.ansible_facts.os_family }}",
          "os": "{{ host_vars.ansible_facts.os_name }}",
          "uptime_days": {{ (host_vars.ansible_facts.uptime_seconds / 86400) | round(2) }},
          "size":
          {
            "name": "{{ host_vars.virtual_machine_size | default('') }}",
            "vCPUs": {{ host_vars.ansible_processor_vcpus }},
            "RAM": {{ (host_vars.ansible_memtotal_mb / 1024) | round | int }},
          },
        },
        {% endif %}
      {% endfor %}
      ]      
  delegate_to: localhost
  run_once: true

  - name: Generate report.html  # noqa: run-once[task]
    ansible.builtin.template:
      src: templates/report.html.j2
      dest: "{{ out_folder }}/report.html"
      mode: "0644"
    delegate_to: localhost
    run_once: true

Compute a list with set_fact and loop

Advanced sample:

- name: Get disks facts
  community.windows.win_disk_facts:
    filter:
      - physical_disk
  no_log: true

- name: Compute RAW disks
  ansible.builtin.set_fact:
    raw_disks: "{{ ansible_disks | community.general.json_query('[?partition_style==`RAW`].number') }}"

# Give the ability to customize drive letters, sample to skip letter F:
# raw_disks: [7, 23, 2]
# custom_data_disks:
#   E: Data
#   G: Backup
#   L: Log

- name: Compute custom list of disk infos
  ansible.builtin.set_fact:
    disks_infos: '{{ disks_infos | default([]) + [{"disk_number": raw_disks[idx], "letter": item.key, "label": item.value}] }}'
  loop: "{{ query('dict', custom_data_disks) }}"
  loop_control:
    index_var: idx
  when:
    - custom_data_disks is defined
    - raw_disks | length > 0

Compute a list with lookup query and jinja statements

See: https://jinja.palletsprojects.com/en/3.1.x/templates/#list-of-control-structures

- name: Determine logstash servers
  ansible.builtin.set_fact:
    logstash_servers: >-
      {% set result = [] -%}
      {% for s in query('inventory_hostnames', 'tag_tech_logstash') -%}
      {%   set dummy = result.append(hostvars[s].name) -%}
      {% endfor -%}
      {{ result }}