Templates

January 7, 2022 - Reading time: ~1 minute

Ansible uses Jenkins templates to build the playbook.
Templates can use variables from the inventory file, or part of the facts collected.

Variables can also be extracted from the inventory.

Additional variables can be set via the --extra-vars flag.

Set default values with:

    node_file: "node-v{{ node_ver }}-linux-x64.tar.xz"

The following playbook downloads a specific version of nodejs to the webservers group:

root@f12d33c83ada:~# cat playbook.yaml
---
- name: Download nodejs
  gather_facts: no
  hosts: webservers
  vars:
    node_host: 'https://nodejs.org/'
    node_ver: "{{ node_version | default('14.18.2') }}"
    node_file: "node-v{{ node_ver }}-linux-x64.tar.xz"
    node_url: "{{ node_host }}/dist/v{{ node_ver }}/{{ node_file }}"
  tasks:
  - name: download nodejs
    get_url:
      url: "{{ node_url }}"
      dest: "/tmp/{{ node_file }}"
      force: yes

Running it to change the version:

root@f12d33c83ada:~# ansible-playbook -i inventory.yaml playbook.yaml --extra-vars node_version=16.13.1

Installing ansible

January 7, 2022 - Reading time: ~1 minute

With package manager in debian-like OS.

apt install python3
apt install python3-pip
apt install python3-argcomplete

Once python is installed, get ansible with:

python -m pip install ansible

Enable auto-complete:

mkdir ~/.ansible-auto
activate-global-python-argcomplete3 --dest ~/.ansible-auto
source ~/.ansible-auto/python-argcomplete.sh
# Remember to include this in your runtimeconfig

Generate config file with al options disabled:

ansible-config init -t all --disabled > ~/.ansible.cfg

Test installation by running ping module locally:

root@d29cb20e2399:~# ansible localhost -m ping
[WARNING]: No inventory was parsed, only implicit localhost is available
localhost | SUCCESS => {
    "changed": false,
    "ping": "pong"
}

Ansible authentication

January 7, 2022 - Reading time: 2 minutes

When running commands remotely, ansible will attempt to use ssh authentication.
The first time you ssh to a new host, it will require to validate the fingerprint:

root@f12d33c83ada:~# ansible all -i 172.17.0.3, -m ping
The authenticity of host '172.17.0.3 (172.17.0.3)' can't be established.
ECDSA key fingerprint is SHA256:ON9GHyGDFBtEvMDi1D6ZTZ+xPBPNsZzBcGmORUIn06g.
Are you sure you want to continue connecting (yes/no/[fingerprint])? ^C [ERROR]: User interrupted execution

This can be disabled by setting the host_key_checking to false:

root@f12d33c83ada:~# fgrep host_key ~/.ansible.cfg
host_key_checking=False

The next step is to decide if you are going to manually input the password every time you run your playbook.
To do this, you'll need to use the --ask-pass flag, and have ssh-pass installed on your system:

root@f12d33c83ada:~# ansible all -i 172.17.0.3, -m ping --ask-pass
SSH password:
172.17.0.3 | FAILED! => {
    "msg": "to use the 'ssh' connection type with passwords or pkcs11_provider, you must install the sshpass program"
}

After installing with apt install sshpass:

root@f12d33c83ada:~# ansible all -i 172.17.0.3, -m ping --ask-pass
SSH password:
[WARNING]: No python interpreters found for host 172.17.0.3 (tried ['python3.10', 'python3.9', 'python3.8', 'python3.7', 'python3.6',
'python3.5', '/usr/bin/python3', '/usr/libexec/platform-python', 'python2.7', 'python2.6', '/usr/bin/python', 'python'])

Finally, to run playbooks without providing any password at all, use ssh-keygen and ssh-copy-id <user>@<host> to use key based authentication.


Running your first remote playbook

January 7, 2022 - Reading time: 2 minutes

Running ansible modules remotely requires python being installed in the remote system.
To show this we need to have a simple inventory with all the hosts:

root@f12d33c83ada:~# cat inventory.yaml
all:
  hosts:
    172.17.0.3:
    172.17.0.4:

Trying to use ping get us:

root@f12d33c83ada:~# ansible all -i inventory.yaml -m ping
[WARNING]: No python interpreters found for host 172.17.0.4 (tried ['python3.10', 'python3.9', 'python3.8', 'python3.7', 'python3.6',
'python3.5', '/usr/bin/python3', '/usr/libexec/platform-python', 'python2.7', 'python2.6', '/usr/bin/python', 'python'])
172.17.0.4 | FAILED! => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python"
    },
    "changed": false,
    "module_stderr": "Shared connection to 172.17.0.4 closed.\r\n",
    "module_stdout": "/bin/sh: 1: /usr/bin/python: not found\r\n",
    "msg": "The module failed to execute correctly, you probably need to set the interpreter.\nSee stdout/stderr for the exact error",
    "rc": 127
}
[...]

You can use the raw module to install python in the remote host:

root@f12d33c83ada:~# ansible all -i inventory.yaml -m raw  -a "apt update && apt install -y python3-minimal"
172.17.0.3 | CHANGED | rc=0 >>
[...]
Processing triggers for libc-bin (2.31-13+deb11u2) ...
Shared connection to 172.17.0.3 closed.

172.17.0.4 | CHANGED | rc=0 >>
Hit:1 http://security.debian.org/debian-security bullseye-security InRelease
Hit:2 http://deb.debian.org/debian bullseye InRelease
[...]

Once python is installed, ansible will find the interpreter:

root@f12d33c83ada:~# ansible all -i inventory.yaml -m ping
172.17.0.3 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}
172.17.0.4 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}

Inventory linter and limit

January 7, 2022 - Reading time: 2 minutes

Ansible requires an inventory with the name of targets (hosts) where it will run the modules.
The inventory allows multiple levels of grouping, and can also include variables for each host.
You can also add specific options like ansible_connection

root@f12d33c83ada:~# cat inventory.yaml
---
all:
  hosts:
    mylocal:
      ansible_connection: local
  children:
    webservers:
      hosts:
        w1:
          ansible_host: 172.17.0.3
          http_port: 80
    database:
      vars:
        ntp_server: time.google.com
      hosts:
        d1:
          ansible_host: 172.17.0.4
        d2:
          ansible_host: 172.17.0.5

You can use a linter to confirm that the syntax of the file is correct before trying to use it:

root@f12d33c83ada:~# pip install yamllint
root@f12d33c83ada:~# yamllint inventory.yaml
root@f12d33c83ada:~#

Then we can run the module in a single host, a group, or use --limit to run it only in one host or a sub-group in a group.

# Single host
root@f12d33c83ada:~# ansible mylocal -i inventory.yaml -m ping
mylocal | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}
# Group
root@f12d33c83ada:~# ansible webservers -i inventory.yaml -m ping
w1 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}
# Group with limit
root@f12d33c83ada:~# ansible database -i inventory.yaml -m ping --limit d1
d1 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}

Ansible facts and first playbook

January 7, 2022 - Reading time: 2 minutes

Every time you run an ansible command remotely, it starts by collecting a list fo facts about the system.
This facts are variables that can be used in the playbook.
collecting facts can be disabled to speed-up the playbook run
We can add custom facts to the target system by writing to /etc/ansible/facts.d a INI file with .fact extension.

Use the setup (ansible.builtin.setup) or gather_facts module to list all the facts:

root@f12d33c83ada:~# ansible webservers -i inventory.yaml -m setup | head
w1 | SUCCESS => {
    "ansible_facts": {
        "ansible_all_ipv4_addresses": [
            "172.17.0.3"
        ],
        "ansible_all_ipv6_addresses": [],
        "ansible_apparmor": {
            "status": "disabled"
        },
        "ansible_architecture": "x86_64",

First playbook

Create a simple playbook that writes a file to the target host:

---
- name: first
  hosts: webservers
  tasks:
    - name: write file
      command: "touch /tmp/test"

Run the playbook on the inventory:

root@f12d33c83ada:~# ansible-playbook -i inventory.yaml playbook.yaml

PLAY [first] ********************************************************************************************

TASK [Gathering Facts] ********************************************************************************
ok: [w1]

TASK [write file] **************************************************************************************
changed: [w1]

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

Here you see how the playbook included only one task, but it ended up running two tasks, because it always start by collecting facts.
We can disable this by setting gather_facts: no in the playbook:

---
- name: first
  gather_facts: no
  hosts: webservers
  tasks:
    - name: write file
      command: "touch /tmp/test"

Then:

root@f12d33c83ada:~# ansible-playbook -i inventory.yaml playbook.yaml

PLAY [first] *******************************************************************************************************

TASK [write file] **************************************************************************************************
changed: [w1]

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