My first experience with Ansible

 

Picture by Christina Morillo from pexels.com
Images by Christina Morillo from pexels.com

Recently, I did some Ansible scripting, and there are some initial struggles to develop good Ansible scripts.

Firstly, Ansible scripts are implemented in YAML format. Hence these scripts can be unmaintainable very quickly if

  1. We do not document each step;
  2. We do not recreate reusable module libraries and/or ansible role. That's we copy and paste code
  3. We do not maintain small Ansible .yml files.
Secondly, I suggest spending some time looking at some good Ansible scripts to accelerate learning. Actually, I spent much time looking at solutions in stackoverflow because there are many interesting questions and answers. And, I found an expert, Vladimir Botka, and learn many things from him. On top these, read through Ansible best practices.

Next, I found that list and object manipulation in Ansible are not natural to me. Here are some of the things that I have learned.

1. Combining dictionaries

I need to set default values to an object (dictionary) so that I can simplify my implementation. Here is the demo script.

---
- name:
  hosts:
    localhost

  vars:
    data: {
      "config1": true,
      "config2": { "enabled": true }
    }

  tasks:
  - name: create default values
    set_fact:
      default_vals: {
        "config1": false,
        "config2": {},
        "dataset": [],
        "extra": "hello-world"
      }

  - name: combine data and default values
    set_fact:
      data: "{{ default_vals | combine(data) }}"

and the result is

ok: [localhost] => {
    "ansible_facts": {
        "data": {
            "config1": true,
            "config2": {
                "enabled": true
            },
            "dataset": [],
            "extra": "hello-world"
        }
    },
    "changed": false
}

the missing attributes, dataset, and extra are added accordingly.

2. Filtering and Mapping

These are very common operations on a list of objects (dictionaries).

---
- name:
  hosts:
    localhost

  vars:
    data: [{
      "hostname": "example.com",
      "ip_address": "10.51.1.4"
    }, {
      "hostname": "sample.com",
      "ip_address": "10.52.1.4"
    }, {
      "hostname": "helloworld.net",
      "ip_address": "10.51.1.51"
    }]

  tasks:
  - name: filter on data
    set_fact:
    filtered_data: "{{ data | selectattr('hostname', 'search', '\\.com$') \
      | map(attribute='ip_address') | list }}"

Here, we filter by hostname ending with .com and get the ip_address of filtered list.

ok: [localhost] => {
    "ansible_facts": {
        "filtered_data": [
            "10.51.1.4",
            "10.52.1.4"
        ]
    }

3. Filtering (in list)

The other common operation is to filter objects when attribute of object is in a list.

---
- name:
  hosts:
    localhost

  vars:
    data: [{
      "hostname": "example.com",
      "ip_address": "10.51.1.4"
    }, {
      "hostname": "sample.com",
      "ip_address": "10.52.1.4"
    }, {
      "hostname": "helloworld.com",
      "ip_address": "10.51.1.5"
    }]
    selected_ip_addrs: [
      "10.51.1.4", "10.51.1.5"
    ]

  tasks:
  - name: filter on data
    set_fact:
      filtered_data: "{{ data | selectattr(\
      'ip_address', 'in', selected_ip_addrs) | list }}"

Here, we filter by ip_address in selected_ip_addrs list.

ok: [localhost] => {
    "ansible_facts": {
        "filtered_data": [
            {
                "hostname": "example.com",
                "ip_address": "10.51.1.4"
            },
            {
                "hostname": "helloworld.com",
                "ip_address": "10.51.1.5"
            }
        ]
    },
    "changed": false
}

4. Concatenating List

In Ansible, it is very easy to concatenate list and get unique values.

---
- name:
  hosts:
    localhost

  vars:
    data1: ["abc", "xyz"]
    data2: ["123", "abc"]


  tasks:
  - name: concatentate list
    set_fact:
      data: "{{ (data1 + data2) | unique }}" 

The duplicate value here is abc.

 ok: [localhost] => {
    "ansible_facts": {
        "data": [
            "abc",
            "xyz",
            "123"
        ]
    },
    "changed": false
}
      

5. Split string by delimiter

I have also encountered situations where I need to split strings. Here is just a simple example.

---
- name:
  hosts:
    localhost

  vars:
    ip_address: "10.51.1.5"


  tasks:
  - name: split ip_address
    set_fact:
      parts: "{{ ip_address.split('.') }}"

  - name: get parts
    set_fact:
      result: "{{ parts[0] + '.' + parts[1] }}"
 

Here, we split IP Address into parts and get only the first and second parts.

 ok: [localhost] => {
    "ansible_facts": {
        "result": "10.51"
    },
    "changed": false
}

6. Loops

Looping through items in list is also also common operation.

---
- name:
  hosts:
    localhost

  vars:
    input: [
      { name: "hello", value" "world" },
      { name: "welcome", value" "world" },
      { name: "hi", value" "world" },
      { name: "hello", value" "world 2" },
    ]

  tasks:
  - name: concatentate list
    with_items: "{{ input }}"
    set_fact:
      data: "{{ ((data | default([])) + [item.name]) | unique }}"

  - name: print result
    debug:
      msg: "{{ data }}"

Here we use default() to set the initial value for data, and then append name of each item to it.

ok: [localhost] => {
    "msg": [
        "hello",
        "welcome",
        "hi"
    ]
}


My Observations

After working with Ansible for about 6 weeks here are my observations

  1. I found myself duplicating Ansible script initially because I do not fully understand the concept of library and roles. After sometime, I started to remove duplicated code and move them to roles or libraries to facilitate code reuse.
  2. I could not understand how set_fact works. There are times when I could not figure out why facts did not in different hosts. After some readings, I figured that facts are tied to hostname. That's a fact that is set in one hostname is not visible to another hostname. Reference

Variables are set on a host-by-host basis just like facts discovered by the setup module.

  1. It is easier to test how things work by creating a small Ansible yml; and run it separately. This helps me to isolate other things and focus on the pieces that I am interested in.
  2. I recommend that you pause development after 3-4 weeks, and set away time to refactor your Ansible scripts. This gives you the opportunity to create reusable roles/libraries, and also improve your scripts.
  3. Use linters for static code checking.
    1. yamllint
    2. ansible-playbook <your.xml> --syntax-check
    3. ansible-lint <your.xml>
My impression on Ansible is great. Its eco-system is huge (thanks to Redhat). It is a very handy tool to install and configure many hosts. It will be great if Ansible can generate a journal on what have been done to different hosts, and generate a consume-able report.

Useful Resources:


Comments

Popular posts from this blog

OpenAI: Functions Feature in 2023-07-01-preview API version

Storing embedding in Azure Database for PostgreSQL

Happy New Year, 2024 from DALL-E