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:
ansible-core
2.14.3 (incl. the Ansible builtin modules)ansible
7.3.0 (incl. the Ansible community packages)ansible-lint
6.13.1
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:
- Inventory – groups of hosts
- Playbook – ordered list of play
- Play – ordered list of tasks
- Task – executed by associated modules written in Python
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:
- “YAML-based inventory, should start with the C(all) group and contain hosts/vars/children entries.”
- Host entries can have sub-entries defined, which will be treated as variables.
- Vars entries are normal group vars.
- “Children are ‘child groups’, which can also have their own vars/hosts/children and so on.”
- File MUST have a valid extension, defined in configuration.
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
:
- YAML data key for
tasks:
andvars:
are removed. - YAML data key
roles:
is added and it lists “base” as its component.
---
- 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:
--tags all
– run all tasks, ignore tags (default behavior)--tags tag1,tag2
– run only tasks with either the tag tag1 or the tag tag2--skip-tags tag3,tag4
– run all tasks except those with either the tag tag3 or the tag tag4--tags tagged
– run only tasks with at least one tag--tags untagged
– run only tasks with no tags
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 |