Bootstrapping WSL: Linux for the Faint of Heart

I've heard WSL referred to as the best of both worlds between Windows and Linux. As a Linux enthusiast, I haven't found it to be quite that, but I recognize that not every developer lives for a tiling window manager with Vim keybindings. Most would prefer minimal interaction with the terminal. For me, the real value of WSL is in team environments of mixed skill sets.

One mistake I've made working on software teams is to overvalue my preferred tools and eschew what others are using. It's a self-limiting attitude for a developer to hold. In a constantly evolving field, restricting your exposure to technology that you, yourself seek out sets you up to be left behind. For a team leader, this mindset untenable. If you don't make a dedicated effort to familiarize yourself with the tools of your team and how they use them, you'll find yourself investing way more time than necessary helping to troubleshoot developer environment issues.

The majority of the time that I need to work with other developers, they are working on Windows and using VS Code. In this post I will explore how I've tried to empower developers to make the best use of this environment and get everyone on the team on a similar baseline. The code for this post is available here.

This post works from the assumption that a fresh WSL Ubuntu instance has been installed on the developer machine, as well as Docker Desktop, and VS Code. These prerequisites will not be covered. For optimal results, it's best to sit down with an inexperienced or CLI averse developer and walk them through the setup process.

One of my aims in creating this bootstrap was to enable the user to make use of it with minimal interaction from the terminal. With that said, initial system setup may also be an optimal time for a gentle primer on the command line. After WSL is installed, consider teaching them some basic commands and putting together a small cheatsheet. The terminal can seem like a mystical black box for voodoo sorcerers. A change in attitude from anxiety to comfort starts with small steps.

Getting Started

The first step is for the developer to clone the repo into WSL. It is possible to do this from the GUI. VS Code has a big blue clone repository button. The user just needs to navigate to their WSL home folder. It is located at \\wsl$\Ubuntu 22.04\home\<user name>, or similar, depending on the distro version.

VS Code allows you to define executable tasks in the file .vscode/tasks.json. This repo comes with one such task defined.

{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "Run WSL Setup",
            "type": "process",
            "command": "bin/install.sh",
            "options": {
                "cwd": "${workspaceFolder}",
            }
        }
    ]
}

This defines a task to run the bin/install.sh script from the repo's root folder. This task can be called by clicking Ctrl + Shift + P and typing Run Task, then selecting RUN WSL Setup from the list of options. I personally find navigating VS Code's menus to be a bit obtuse and convoluted. There is an extension available named Task Runner which gives the user a quick way to navigate a project's custom tasks. I recommend having the user install this extension and just clicking on the task to run it.

VS Code Task Runner Window

This combination of task definitions and the task runner extension can come in really handy when your project has some complex scripts that need to be occasionally run. I've found it particularly handy when developing in a Docker environment. If a developer needs to run some command inside of a container, a button click is way easier than docker exec -it...

A Closer Look at the Install Script

The contents of bin/install.sh are fairly short. It first checks to see if pip is installed. If not it installs it. Next it installs Ansible via pip. Then an Ansible playbook is run. This is really the heart of the script which we will dive into shortly. Lastly, it generates an SSH key and instructs the user to add it to their GitHub profile. I added this bit after I was shocked to discover that my belief that interacting with git via ssh is baseline developer knowledge, proved to be false.

#!/usr/bin/env sh

# check for pip and install it if missing
python3 -m pip -V
if [ $? -ne 0 ]; then
    sudo apt-get update
    sudo apt-get install python3-pip
fi

# install ansible and run main playbook
sudo python3 -m pip install ansible
ansible-playbook playbooks/main.yml

# generate ssh key
ssh-keygen -t ed25519

echo "The following is your public ssh key. Copy it and add it to your GitHub profile."
echo "learn more here: "
echo "https://docs.github.com/en/authentication/connecting-to-github-with-ssh/adding-a-new-ssh-key-to-your-github-account"
cat ~/.ssh/id_ed25519.pub

Dissecting the Ansible Playbook

I've developed a bit of a love-hate relationship with Ansible over the years, but I think it is an excellent choice for development environment bootstrapping. If you are not familiar with Ansible, it allows you to provision a server through yaml configuration files. Their excellent documentation is available here.

Let's have a look at the main playbook file.

# main/playbook.yml
---
- hosts: localhost
  become: yes
  roles:
    - { role: base, tags: ['base'] }
    - { role: ssh, tags: ['ssh'] }
    - { role: dotfiles, tags: ['dotfiles'] }
    - { role: editors, tags: ['editors'] }
    - { role: nettools, tags: ['nettools'] }
    - { role: ntp, tags: ['ntp'] }
    - { role: sudo, tags: ['sudo'] }
  vars_prompt:
    - name: user_name
      prompt: "Enter your first and last name"
      private: false

    - name: user_email
      prompt: "Enter your email address"
      private: false

This playbook applies seven roles: base, ssh, dotfiles, editors, nettools, ntp, and sudo. Most of these roles make use of variables set in playbooks/group_vars/all/defaults.yml. The name and email variable prompts are used for global git configuration. I will just give a cursory explanation of the various roles. Your team's needs will be unique. This repository is intended as a starting point.

Base Role

The main file for the base role is in playbooks/roles/base/tasks/main.yml. This is broken down into related actions.

Packages: This installs some baseline packages. The list of packages is defined in the group_vars file's base_packages variable. You'll want to curate your own base packages. A couple I find especially helpful are python-is-python3, this package configures the python and pip commands to link to python3 and python3-pip respectively. The other one that is often handy in a WSL environment is dos2unix. This converts a file's Windows style return and line feed, \r\n line endings to the Linux line feed \n. Incongruent line endings is a source of friction when working in a mixed environment. This helps to alleviate that.

Shell: This configures the shell environment for the user. It installs both bash and zsh. By default it sets the user's shell to zsh configuring it to use oh-my-zsh and starship. These are my preferred tools. You will have your own. It's important to note that there is a fine line between setting up helpful defaults and top-down decrees. Have a conversation with your team and make tools opt-in where it makes sense for you.

SSH Role

This role makes sure that both the OpenSSH server and client are installed and configured as well as a few other ssh niceties.

Dotfiles Role

This installs GNU Stow and uses that to add some configuration files. Stow is essentially a Linux symlink manager. It Can be a really handy way to manage Linux dotfiles by storing them all in a central directory and creating symlinks to where their respective programs expect them to live. You can then put the central directory under version control. Again, dotfiles is another area where it can be easy to be heavy-handed. This role adds some really nice things for a developer like starting an ssh agent and setting up time sync.

Editors Role

This role configures a default editor, in this case neovim. It's helpful to have something like this setup if you ever need to walk someone through editing a file directly in WSL. For the most part this shouldn't be necessary as the files are accessible from Windows land.

Nettools Role

This just installs some network analysis and management tools. You devs may never actually need these, but it's helpful to have installed if they need help troubleshooting some network issue.

NTP Role

This installs and configures a Network Time Protocol utility. In this case it's actually Chrony not NTP. This makes sure that the WSL system clock is synced with an internet time server. The dotfiles role makes sure this is actually running. WSL has nothing like Systemd, so all services must be started when the session starts. It's a good idea to do this on the Windows side as well. I've had issues where a developer's time sync issues caused certs to be viewed as expired. Instructions can be found here.

Sudo Role

This role creates a "wheel" group, grants that group sudo privileges, adds the user to that group, and enables passwordless sudo. Whether or not you want to configure passwordless sudo for the user is a judgement call. To me this is essentially just their private VM, so the risk is worth the headaches it causes. I've found that far too often certain developers have no idea what their WSL password is.

Conclusion

This repository is really just supposed to act as a starting point to get you thinking how you can better anticipate your team's needs and how to sidestep some issues before they happen. You'll need to dig through the files to really see what's going on here. It's not a ton to look at and hopefully it gives you some ideas of your own.