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