Ansible (1)

Date: 2024/02/17 (initial publish), 2024/02/23 (last update)

Source: en/note-00065.md

Previous Post Top Next Post

TOC

I realize that it is about time to getting organized for my system setup in a way better than making notes and writing shell scripts.

I decided to deploy Ansible for localhost only with CLI to help me get organized and learn Ansible.

Since upstream Ansible documentation addresses more generic remote host usages and is too much to digest, this memo should be a good minimal use case for me to get started. (Somehow, it is sometimes easier to look into the Python module directory to find and understand how to use ansible.)

Debian 12 Bookworm packages are based on:

FYI: Test code github repo

Installation of Ansible

$ sudo apt update
$ sudo apt install ansible ansible-lint

What is Ansible

Ansible is an IT Automation Platform which uses declarative configuration file called Playbooks.

Ansible Playbooks are lists of tasks that automatically execute for one’s specified inventory.

Here:

Ansible Playbook can also be a Roles—bundles of tasks. That is addressed later.

ansible(1) and ansible-playbook(1) CLI

Key difference of ansible(1) and ansible-playbook(1) is their target.

The ansible(1) command operates on host_name_pattern as:

$ ansible host_name_pattern
 ...

The ansible-playbook(1) command operates on one or more Playbook as:

$ ansible-playbook playbook1 [playbook2 ...]
 ...

Create template ansible.cfg with ansible-config

Let me create template ansible.cfg with ansible-config.

$ cd /path/to/ansible-config-data/
$ ansible-config init --disabled >ansible.cfg

Let me keep the working directory placed on /path/to/ansible-config-data/ from here on.

Create inventory with localhost

Update ansible.cfg:

$ sed -i -e '/^;inventory/s/^*$/inventory=inventory.yml/' ansible.cfg

This creates ansible.cfg as (after removing non-essential comments and empty lines):

[defaults]
# (pathlist) Comma separated list of Ansible inventory sources
inventory=inventory.yml

Let me create simplest possible inventory.yml to define groups of hosts on which ansible operates as:

---
all:
  hosts:
    localhost:
      ansible_connection: local

(This is localhost only and explicitly uses local connection. When I wish to support remote clients with ssh, I need to update this.)

This helps to quiet “[WARNING]” when I execute following playbook examples.

It’s content requirement is found in /usr/lib/python3/dist-packages/ansible/plugins/inventory/yaml.py as:

Use of ansible-playbook command (localhost)

Let me learn to use playbook via ansible-playbook command first with APT operation.

Playbook to install “screen”

Here is an example playbook pb_screen.yml to install “screen” package.

---
- name: Example for ansible.builtin.apt with screen
  hosts: localhost
  connection: local
  gather_facts: false
  become: true
  tasks:
    - name: Install "screen" package
      ansible.builtin.apt:
        name: screen
        state: present

This playbook consists of 1 play and contains a single task definition which uses ansible.builtin.apt module.

The module executable itself is written in Python.

Let me run this playbook on the localhost without “screen” package:

$ ansible-playbook pb_screen.yml
PLAY [Example for ansible.builtin.apt with screen] *******************************

TASK [Install "screen" package] **************************************************
changed: [localhost]

PLAY RECAP ***********************************************************************
localhost                  : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

So package is installed and system is changed.

Playbook to install “screen” again

Let me run the same playbook again:

$ ansible-playbook pb_screen.yml

PLAY [Example for ansible.builtin.apt with screen] *******************************

TASK [Install "screen" package] **************************************************
ok: [localhost]

PLAY RECAP ***********************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Package is not installed and system is unchanged. This behavior is what its idempotency guarantee.

Why not to use “latest”

It’s tempting to change from “present” to “latest” in this example playbook to ensure updated version for “screen”. But it is not a good idea. It can be verified with ansible-lint:

$ sed -i 's/present/latest/' pb_screen.yml
$ ansible-lint pb_screen.yml
WARNING  Listing 1 violation(s) that are fatal
package-latest: Package installs should not use latest.
pb_screen.yml:8 Task/Handler: Install "screen" package

Read documentation for instructions on how to ignore specific rule violations.

              Rule Violation Summary
 count tag            profile rule associated tags
     1 package-latest safety  idempotency

Failed after moderate profile, 2/5 star rating: 1 failure(s), 0 warning(s) on 1 files.

The key word is its tag “package-latest”. Its long explanation can be found under its module directory /usr/lib/python3/dist-packages/ansiblelint/rules as markdown files or on Ansible Lint Documentation: Rules.

Playbook to install multiple packages (tasks)

Here is another example playbook pb_many.yml to upgrade, install, and remove packages.

---
- name: Example for ansible.builtin.apt with many packages
  hosts: localhost
  connection: local
  gather_facts: false
  become: true
  tasks:
    - name: Upgrade the whole system
      ansible.builtin.apt:
        upgrade: safe
    - name: Install packages
      ansible.builtin.apt:
        name: ["screen", "sudo"]
        state: present
    - name: Install more packages
      ansible.builtin.apt:
        name:
          - mc
          - aptitude
          - vim
        state: present
    - name: Remove packages
      ansible.builtin.apt:
        name: ["nano"]
        state: absent

This playbook consists of 1 play and contains 3 task definitions all of which use ansible.builtin.apt module.

Here, 2 style of YAML lists are used. Both works fine.

$ ansible-playbook pb_many.yml 

PLAY [Example for ansible.builtin.apt with many packages] **********************

TASK [Upgrade the whole system] ************************************************
ok: [localhost]

TASK [Install packages] ********************************************************
ok: [localhost]

TASK [Install more packages] ***************************************************
ok: [localhost]

TASK [Remove packages] *********************************************************
ok: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=4    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

Playbook to install multiple packages (tasks+vars)

Here is another example playbook pb_many_vars.yml to upgrade, install, and remove packages.

---
- name: Example for ansible.builtin.apt with many packages
  hosts: localhost
  connection: local
  gather_facts: false
  become: true
  vars:
    package_name_install:
      - screen
      - sudo
      - mc
      - aptitude
      - vim
    package_name_remove:
      - nano
  tasks:
    - name: Upgrade the whole system
      ansible.builtin.apt:
        upgrade: safe
    - name: Install packages listed in package_name
      ansible.builtin.apt:
        name: "{{ package_name_install }}"
        state: present
    - name: Remove packages
      ansible.builtin.apt:
        name: "{{ package_name_remove }}"
        state: absent

This playbook consists of 1 play and contains 1 list variable definition for package_name and 3 task definitions all of which use ansible.builtin.apt module.

Here, playbook variable substitution is used. Be careful for the use of quotation marks.

$ ansible-playbook pb_many_vars.yml 

PLAY [Example for ansible.builtin.apt with many packages] **********************

TASK [Upgrade the whole system] ************************************************
ok: [localhost]

TASK [Install packages listed in package_name] *********************************
ok: [localhost]

TASK [Remove packages] *********************************************************
ok: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

Playbook to install multiple packages (use roles)

Although single page playbook works fine for small task, writing one big playbook in the base directory is messy.

Let me split above playbook into multiple files with pb-main.yml in the base directory.

 $ tree
.
├── ansible.cfg
├── inventory.yml
├── pb_main.yml
└── roles
    └── base
        ├── tasks
        │   ├── install.yml
        │   ├── main.yml
        │   ├── remove.yml
        │   └── upgrade.yml
        └── vars
            └── main.yml

Here in this new pb_main.yml:

---
- name: Example for ansible.builtin.apt with many packages
  hosts: localhost
  connection: local
  gather_facts: false
  become: true
  roles:
    - base

Here, listed value(s) for roles matches the directory name under the roles/ directory.

Original YAML data under tasks: are essentially moved to roles/base/tasks/main.yml using inclusion of 3 files.

roles/base/tasks/main.yml:

---
- import_tasks: upgrade.yml
- import_tasks: install.yml
- import_tasks: remove.yml

Here, import_tasks is used to pre-process the statement statically at the time whole playbooks are parsed and includes referred tasks.

(Similar include_tasks is used to process the statement dynamically every time this playbook statement is executed and includes referred tasks.)

roles/base/tasks/upgrade.yml:

---
- name: Upgrade the whole system
  ansible.builtin.apt:
    upgrade: safe
  tags: upgrade

roles/base/tasks/install.yml:

---
- name: Install packages listed in package_name
  ansible.builtin.apt:
    name: "{{ package_name_install }}"
    state: present
  tags: install

roles/base/tasks/remove.yml:

---
- name: Remove packages
  ansible.builtin.apt:
    name: "{{ package_name_remove }}"
    state: absent
  tags: remove

All 3 split task files have tags: key specified to offer flexiblity to the playbook as explained in the next section.

Original YAML data under vars: are moved to roles/base/vars/main.yml

roles/base/tasks/main.yml:

---
# vars file

package_name_install:
  - screen
  - sudo
  - mc
  - aptitude
  - vim

package_name_remove:
  - nano

In the above, the entry point data file for each key such as tasks and vars which match the parent directory name is required to use the file name main.yml.

$ ansible-playbook pb_main.yml

PLAY [Example for ansible.builtin.apt with many packages] ***************************

TASK [base : Upgrade the whole system] **********************************************
ok: [localhost]

TASK [base : Install packages listed in package_name] *******************************
ok: [localhost]

TASK [base : Remove packages] *******************************************************
ok: [localhost]

PLAY RECAP **************************************************************************
localhost                  : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Play a part of Playbook with tag

Tags let me execute only selected portion of tasks easily.

Since I already added tags: keys in the previous example, let me selectively play a part of playbook.

 $ ansible-playbook -t install pb_main.yml

PLAY [Example for ansible.builtin.apt with many packages] ***************************

TASK [base : Install packages listed in package_name] *******************************
ok: [localhost]

PLAY RECAP **************************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

ansible-playbook offers five tag-related command-line options:

Note: Considering the default behavior without specifying -t option feature being -t all, this tag thing should be used only to narrow the execution scope. If I want to add extra codes such as debug echos which should be normally skipped, I should use conditional approach controlled by the value of some variables to skip them. The wording of the initial phrase of this section was chosen without using skip to emphasize this fact.

Use of ansible command (localhost)

I can test modules directly from ansible CLI. For example ansible.builtin.apt module can be tested from any directory as:

 $ ansible -i localhost, -c local localhost -m ansible.builtin.apt -a "name=screen state=present"
localhost | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "cache_update_time": 1708403247,
    "cache_updated": false,
    "changed": false
}

Please note the use of comma in -i locales, option for the host list. (This is not the path string of the inventory host file. Oh well … Tricky …)

If my working directory is still placed on /path/to/ansible-config-data/ where properly configured ansible.cfg exists, the above can be simplified as:

 $ ansible localhost -m ansible.builtin.apt -a "name=screen state=present"
 ...
Previous Post Top Next Post