Ansible
Installation
user@ansible:~# python3 -mvenv ~/virtenv
user@ansible:~# . ~/virtenv/bin/activate
(virtenv) user@ansible:~# python -mpip install ansible
Collecting ansible
Downloading ansible-8.7.0-py3-none-any.whl (48.4 MB)
|████████████████████████████████| 48.4 MB 4.2 MB/s
Collecting ansible-core~=2.15.7
...
(virtenv) user@ansible:~#
inventory
add-hoc commands
tasks
plays
playbooks
roles
modules
Prerequisites
Inventory
First we have to create an inventory and include the nodes we want to manage. The simplest method is to create an ini-file. Each line defines a node and is in the format shown below:
[<nodegroup>]
<node_name> ansible_host=<ip_address>
[<nodegroup>] : logical group to reference a group of hosts
<node_name> : node_name used in ansible to reference a node
<ip_address> : ip-address to reach the node
ansible_host : ansible variable holding the hostname/ip
Logical Groups:
local
controller
router
switches
hosts
Nodes in group hosts:
dc1
dc2
slapd1
slapd2
ns1
ns2
h3
h4
h5
h6
h7
File Structure
To organize our ansible files we will use the following file structure:
inventories : inventory files
playbooks : playbook files
roles : role folders
We'll explain roles later in this document.
Ad-Hoc Commands
We can use ad-hoc commands to send commands to a bunch of hosts, e.g. to check if they are reachable. We still have to use an inventory to refer to the nodes. In the following example we check if all hosts in the inventory are up and reachable:
-i <inventory_file>
-m <module>
-o / --one-line
Tasks
A task is the smallest unit in ansible. It's running a single command and its definition is usually stored in a file. An ad-hoc command is also considered as a task that is created on the fly via the command line interface. Tasks could be as simple as:
creating a file
installing a package
single command/job
e.g. ad-hoc command
yaml is used for the task definition in a file
Ansible Task Definition
- name: task name
copy:
src : /tmp/file1
dest : /tmp/file2
The bash equivalent command to the ansible task would look like this:
bash$ cp /tmp/file1 /tmp/file2
Plays
A play is the next bigger unit that consists of multiple tasks ( at least one ) that are needed to accomplish a bigger goal, e.g.
play: get a webserver up and running
task 1: install the server software (apache)
task 2: configure the server (httpd.conf)
task 3: start server and enable auto-start (systemd)
multiple tasks working together
Ansible Play Definition
- name: play 1
hosts: localhost
tasks:
- name: task 1
file:
name : /tmp/dir2
state: directory
- name: task 2
copy:
src : /tmp/file
dest : /tmp/dir2/file2
The bash equivalent command execution would be:
bash$ mkdir /tmp/dir2
bash$ cp /tmp/file1 /tmp/dir2/file2
Playbooks
The next bigger unit would be a playbook that consists of multiple plays, e.g.
playbook: get webapp1 up and running on siteX
play 1: get a database up and running (needed by the webapp)
play 2: get the webapp1 up and running
play 3: protect the webapp1 with a WAF
multiple plays working together
# Playbook 1
- name: play 1
hosts: host1
tasks:
- name: task 1
file:
name : /tmp/dir2
state: directory
- name: task 2
copy:
src : /tmp/file
dest : /tmp/dir2/file2
- name: play 2
hosts: host2
tasks:
- name: task 1
yum:
name: httpd
- name: task 2
yum:
name: php
a playbook is just a yaml file consisting of multiple plays.
play1: runs on host1
play2: runs on host2
Roles
A role is like a playbook that is written in a reusable way. In a sense it's similar to a function in programming. It can be used to deploy a specific software that could be run on multiple operating systems, so we hide the complexity in the role and just apply it in a play, e.g.
to get apache up and running on several host running each ad different os ( centos, debian, arch, gentoo, ... )
generalised playbook that can be (re)used in another playbook.
A role is divided into several files within the following directory structure:
In the defaults directory we store default variables/values.
In the handlers directory we keep ansible tasks that are used as actions within the role, e.g. reload httpd.conf when it changed.
In the tasks directory we define all the tasks for the role.
In the templates directory we store e.g. jinja templates used to create config files (httpd.conf)
Main Directories:
defaults
handlers
tasks
templates
Other Directories:
vars
plugins
meta
tests
Below is an example how roles are used in a playbook:
# Playbook
- name: install apache
hosts:
- host1
- host2
roles:
- roles/httpd
- roles/php
host1: centos9
host2: debian11
Modules
A module is a command in ansible that is used in tasks. It's usually a python script that is run on the target and if written in an idempotent way it makes sure the result of that script execution is always the same. Usually the command expects some arguments to use, e.g. filepath, username, ...
task 1:
command: file
arguments:
name: /etc/motd
owner: root
...
command that runs on the target
arguments passed to the module
We have used modules in our examples before when we used tasks. We called the module we used as command. The following example is using the file module with the arguments name and state to indicate that we want the directory /tmp/dir2 present on the target host.
- name: task 1
file:
name : /tmp/dir2
state: directory
Writing custom Modules
We are going to create a simple hello world module that runs on the target and returns the json string: { "hello" : "world" }.
# ./library/hello.py
from ansible.module_utils.basic import *
def main():
module = AnsibleModule(argument_spec={}) # declare the code to be a module
res = {"hello":"world"} # prepare the response as json
module.exit_json(changed=False, meta=res) # return the response
if __name__ == '__main__': # make the script run standalone
main()
We would use that module in a playbook like so:
---
# ./playbooks/playbook.yml
- name: playbooks.hello
hosts: localhost
tasks:
- name: playbook.hello.tasks.module.run
hello:
This is how a run would look like. We use the environment variable ANSIBLE_LIBRARY to include our module.