Ansible Week - Part 4
Ansible is powerful, so let's put together a real-world example of how to use it for installing some software.
We have recently been blessed with a new crypto library aimed at maintaining strong security that doesn't depend on the difficulty of factoring composite numbers.
It's so new, there isn't a package for it, and its installation steps are quirky, to say the least. It boils down to:
- Install some pre-requisite tools (OpenSSL, GMP, Python3, gcc)
- Create a new user, libpqcrypto
- Fetch the software as that user
- Create some symlinks
- Compile the library
Ansible was made to handle all of this, and to demonstrate the power of Ansible roles in our playbook, we're going to split the prerequisite installation steps out into their own separate pieces, or "roles". Roles are useful for when we want a set of commands we can use and reuse to set up a build environment, even if we end up not using that environment to build this exact libpqcrypto library again in the future.
By putting your work into roles, Ansible allows you to group your tasks into distinct phases and sort them based on your defined dependencies between them. In other words, if you have a role called "fasten-seatbelt", you can define other roles as unique dependencies for it, maybe ones called "sit-in-seat", "have-keys-in-hand", and "buy-a-car". Each of these roles are generalizable into other things, so if you ever write "ride-rollercoaster", you can reuse the tasks of "sit-in-seat", though perhaps without the car-buying dependency.
Maybe we should just build the role and show you.
First, we set up a target host. We've done this before, but for this exact example we're going to install our OS (a Debian-based Linux in this case), patch it to current, add our ansible user, and create our OpenSSH key:
sudo apt install -y openssh-server sudo groupadd ansible sudo useradd -g ansible -m ansible ssh-keygten -t ed25519 -N '' -q -f ~/.ssh/id_ed25519 cd .ssh cp -p ./id_ed25519.pub ./authorized_keys.new chmod 0600 ./authorized_keys.new mv ./authorized_keys.new ./authorized_keys exit sudo visudo #add line: # ansible ALL=(ALL:ALL) NOPASSWD: SETENV: ALL
Sync this key, ~ansible/.ssh/id_ed25519 to your ansible host and build your inventory file:
[libpqcrypto] 10.0.0.4 [libpqcrypto:vars] ansible_become_method=sudo ansible_ssh_user=ansible ansible_ssh_port=22 ansible_ssh_private_key_file=/path/to/libpqcrypto/id_ed25519
We run ansible -i ./hosts.libpqcrypto -m ping libpqcrypto and our ping got ponged, so we can write our first role. It's a .YML file outlining which packages we want to install before we go about doing anything else with our machine:
---
- name: install compiler and libpqcrypto pre-reqs (apt)
become: yes
apt:
args:
name: "{{item}}"
state: present
cache_valid_time: 86400
update_cache: yes
with_items:
- build-essential
- gcc
- libssl-dev
- libgmp-dev
- make
- python3
when: ansible_pkg_mgr == "apt"
This is just a normal Ansible playbook. We turn it into a role by putting it in an exact location on our Ansible machine: ./roles/libpqcrypto-prereqs/tasks/main.yml.
This creates a role called "libpqcrypto-prereqs" we can reference in our role that will fetch the libpqcrypto source, configure the host to create a user, make some symbolic links, and compile the code as per the instructions on the web site. Let's make another role to do these steps. If our previous role has run successfully, we know we have our compiler and dev libraries on the target host and can just do the other steps. So we make a role, "libpqcrypto-build", and put this into "./roles/libpqcrypto-build/tasks/main.yml":
---
- name: create group
become: yes
group:
args:
name: libpqcrypto
state: present
- name: create user
become: yes
user:
args:
name: libpqcrypto
createhome: yes
group: libpqcrypto
home: /home/libpqcrypto
shell: /bin/false
state: present
- name: fetch latest version string
become: yes
become_user: libpqcrypto
get_url:
args:
url: https://libpqcrypto.org/libpqcrypto-latest-version.txt
dest: /home/libpqcrypto/libpqcrypto-latest-version.txt
validate_certs: false # ouch
- name: read latest version string
shell: cat /home/libpqcrypto/libpqcrypto-latest-version.txt
register: version
- name: stat libpqcrypto file
stat:
args:
path: libpqcrypto-{{version.stdout}}.tar.gz
register: st
- name: fetch libpqcrypto
become: yes
become_user: libpqcrypto
get_url:
args:
url: https://libpqcrypto.org/libpqcrypto-{{version.stdout}}.tar.gz
dest: /home/libpqcrypto/libpqcrypto-{{version.stdout}}.tar.gz
validate_certs: false
when: st.stat.exists == False
# never use unarchive
- name: untar libpqcrypto
become: yes
become_user: libpqcrypto
shell: tar -xzf /home/libpqcrypto/libpqcrypto-{{version.stdout}}.tar.gz
args:
chdir: /home/libpqcrypto/
creates: /home/libpqcrypto/libpqcrypto-{{version.stdout}}/
- name: create symlinks
become: yes
become_user: libpqcrypto
file:
args:
src: /home/libpqcrypto
dest: /home/libpqcrypto/libpqcrypto-{{version.stdout}}/{{item}}
owner: libpqcrypto
group: libpqcrypto
force: yes
state: link
with_items:
- link-build
- link-install
- name: remove clang compiler option
become: yes
become_user: libpqcrypto
lineinfile:
args:
path: /home/libpqcrypto/libpqcrypto-{{version.stdout}}/compilers/c
regexp: "^clang.*"
state: absent
- name: timestamp
shell: date
register: timestamp
- name: start compile libpqcrypto
debug:
args:
msg: "{{timestamp.stdout}}"
- name: compile libpqcrypto
become: yes
become_user: libpqcrypto
shell: ./do
args:
chdir: /home/libpqcrypto/libpqcrypto-{{version.stdout}}
- name: timestamp
shell: date
register: timestamp
- name: end compile libpqcrypto
debug:
args:
msg: "{{timestamp.stdout}}"
There's a lot going on here, but you can pretty much tease out what each of these steps is doing to your target host. Many Ansible tasks have an argument called "state" that can be either "present" or "absent". The task doesn't necessarily perform the work if it's already been done, so what we're really setting up is a "configuration outlining the desired state of the system" or a "desired state configuration" for short. This is a term I just now invented all by myself. You're welcome.
We ensure there's a group called "libpqcrypto" and a user in that group with the same name. We fetch the libpqcrypto version string and, optionally, fetch that particular version of the software package if that tarball doesn't exist on the target host. We check the existence of that file with the "stat" module and use a "when:" conditional to tell Ansible to run that task only if it needs to satisfy the condition.
Then we create some symlinks with the "file" module, and then we go off script for a second to make a one-line change to the C compilers setting to remove the clang line. This can be skipped if the host has clang installed. We could tailor this task with a "when:" conditional, either checking for "/usr/bin/clang" on the machine, or by comparing the task against what Ansible determines the machine's OS to be. You can pull the list of values that Ansible checks by running the "setup" module: "ansible -i ./hosts.file -m setup hosts-group-name".
We print the date by creating a "register" called "timestamp" populated with the output of the date command. We do this again when the compile task runs and that gives us a notice of how for long the task in between ran.
Finally, (except for that last timestamp task) we compile the libpqcrypto software by running ./do (with the "shell" module), as the libpqcrypto user (with become_user), in a specific directory (identified with the "chdir" argument).
Great! But how do we actually run this role? We need to point out that this role has a dependency on installing the pre-requisites, so we list them in "./roles/libpqcrypto-build/meta/main.yml":
---
dependencies:
- { role: libpqcrypto-prereqs }
Then we put put our "libpqcrypto-build" role, complete with its listed pre-req role(s), into a new playbook file, libpqcrypto.yml:
---
- hosts: libpqcrypto
roles:
- { role: libpqcrypto-build }
tasks:
- group_by:
args:
key: "{{ansible_distribution}}"
Note that we never call the libpqcrypto-prereqs role directly. We call one role in the "roles:" section and with its dependency file in .../meta/main.yml Ansible figures out what to do and in which order to do it.
Naturally you can make this a fairly complicated web of dependencies: "car" requires "tires", "tires" requires "hubcap", and so on as I explained earlier. I haven't seen Ansible have a problem with sorting a dependency chain so long as it can all eventually be collapsed into a linear sequence per host.
Note also that we execute our tasks by groups of their distributions. I started doing this in order to avoid having to create specific conditionals for various target hosts:
- hosts: webservers
roles:
- { role: debian_stock_config, when: ansible_os_family == 'Debian' }
We can set up different roles on different machines based on how we know they'll need to execute our playbook tasks. If all your machines are homogenous, you can skip grouping your tasks, since you won't have variants. "group_by" is much more powerful than this, since you can use it to create groups on the fly by adjusting the value of "key".
No comments:
Post a Comment