If you’ve not used it,ASDF is a tool version manager that helps ensure that you - and your teams -
are all using the same tool version. It is a simple and elegant solution to the problem that nearly all development
teams find themselves in, where different microservices (or different projects) all have differing versions of tooling
that they have been written with. ASDF cuts through that problem by allowing you to define a .tool-versions
file on
a per-directory basis to keep everyone on the same version.
ASDF is also incredibly simple and extendable. You install a plugin that tells ASDF how to install a given tool or program, and then ASDF manages it from there. The plugin syntax is fairly straightforward and there are a number of examples if you needed to add to the ASDF plugin library.
My particular use case here is to setup a development virtual machine or container for my use; I personally use Proxmox as my hypervisor, but this process will work with any provider that allows you to SSH into your new system.
To do this, we are leveraging Ansible to do the heavy lifting. If you’re not familiar with Ansible, it is an IT configuration management tool that is written in Python and leverages SSH to perform configuration management tasks. We will be using Ansible to connect to our newly provisioned systems and configure them according to our tastes. Note that this is my preferred configuration; yours will likely be different.
Ansible starts with an inventory of the systems that you wish to act on; this is a file that contains the system names along with additional information required by Ansible. In this example, we are acting on a system with the name of “host_name” which is part of “group_name” (you can disregard groups for now). There are two configuration tweaks that I have added - I want Ansible to always use Python3 on the host system, and I don’t need strict host checking since nearly all of my systems are internal (you should decide if this is the right call for you and your use case, however).
[all:vars]
# Specify the Python interpreter
ansible_python_interpreter=/usr/bin/python3
# Automatically accept host SSH keys
ansible_ssh_extra_args='-o StrictHostKeyChecking=no'
[group_name]
host_name
The next thing we do is define the variables that Ansible will use for deployment; the example file provided below
can be followed. The ASDF website can be used to check plugin names and versions; or you can setup ASDF locally in
order to build out the .tool-versions
you wish to push to the new systems.
# User configuration
user_name: '<username>'
user_group: '<group>'
user_shell: '<shell_path>'
sudoers_content: '%<group> ALL=(ALL:ALL) NOPASSWD:ALL'
authorized_keys_url: '<authorized_keys_url>'
# ASDF configuration
asdf_init_shell: true
asdf_version: '<asdf_version>'
asdf_user: '<asdf_user>'
asdf_group: '<asdf_group>'
asdf_default_tool_versions_file: '<tool_versions_file_path>'
# ASDF plugins configuration
asdf_plugins:
- name: '<plugin1>'
versions:
- '<version1>'
global: '<global_version1>'
- name: '<plugin2>'
versions:
- '<version2>'
global: '<global_version2>'
# Add more plugins as needed
# Required packages
required_packages:
- '<package1>'
- '<package2>'
- '<package3>'
# Add more packages as needed
Finally, there is the Ansible playbook; this does the heavy lifting for our process. The playbook contains inline comments to help explain what each step is doing, but in short this playbook:
.tool-versions
file.# This playbook is divided into two parts. The first part is executed as root and the second part as the user 'jschmidt'.
# Part 1: Executed as root
- hosts: all
remote_user: root
gather_facts: true
become_method: sudo
become: true
tasks:
# Include our necessary variables
- name: Include variables from vars.yaml
include_vars: vars.yaml
# Update and upgrade apt packages
- name: Update and upgrade apt packages
become: true
apt:
upgrade: yes
update_cache: yes
cache_valid_time: 86400 # Cache valid for one day
- name: Make sure we have a '{{ user_group }}' group
group:
name: "{{ user_group }}"
state: present
- name: Add the user '{{ user_name }}' with a specific uid and a primary group of '{{ user_group }}'
user:
name: "{{ user_name }}"
comment: User
group: "{{ user_group }}"
shell: "{{ user_shell }}"
- name: sudo without password for '{{ user_group }}' group
copy:
content: "{{ sudoers_content }}"
dest: "/etc/sudoers.d/{{ user_name }}"
mode: 0440
- name: Set authorized keys taken from url
ansible.posix.authorized_key:
user: "{{ user_name }}"
state: present
key: "{{ authorized_keys_url }}"
- name: Install required system packages
apt: name={{ item }} state=latest update_cache=yes
loop: "{{ required_packages }}"
- name: Create .tool-versions file
template:
src: tool-versions.j2
dest: "/home/{{ user_name }}/.tool-versions"
become: yes
become_user: "{{ user_name }}"
# Part 2: Executed as our user
- hosts: all
remote_user: root
gather_facts: true
become_method: sudo
become: true
roles:
- role: cimon-io.asdf
tasks:
- name: Include variables from vars.yaml
include_vars: vars.yaml
There is one additional file that is important in this implementation, this is the templates/tools-versions.j2
template file that is used to build out the .tool-versions
file.
Once everything is setup appropriately, you simply run the playbook.
$ ansible-playbook -i ./ansible-hostfile asdf-playbook.yaml