Polished Ruby Programming Book Review

(I received a free copy of Polished Ruby Programming from the publishers in conjunction with co-hosting the Ruby Rogues podcast.)

The Ruby language has two sides and both of them are trains.

One of the trains is a famous, bright red locomotive with a smiling “Thomas the Tank Engine” face. The Rails Train pulls carriages full of happy, productive programmers that wave from open-top cars. As the Rails Train speeds past downtrodden Java developers, pessimistic Pythonistas and groaning Node coders, DHH and his merry band of Rails Core coal-shovellers blow the train’s whistle, drawing the onlookers’ attention to the huge pile of billion-dollar startups in the cargo car.

In the distance, someone has tied Javascript to the tracks and the train appears to be speeding up.

The other Ruby train is very different. There are no Rails – it appears to use some kind of magnetic levitation. There is no smiling face and no open-top cars. The windows are closed to prevent accidents. Most of the time, it runs underground.

Choo choo! Make your framework disappear.

Polished Ruby is a book about this train. It’s a book from the Ruby Underground and, appropriately, it’s a bit of a gem.

True to the title, the book describes intermediate and advanced techniques that will improve your Ruby projects. However, the real value of this book is its fresh perspective on software development and maintenance. Many of the ideas discussed in Polished Ruby have not appeared in similar Ruby books. Popular views in the Ruby community are challenged and every challenge is made by examples that demonstrate the underlying logic.

Readers will witness a controlled demolition of SOLID principles. Rubocop gets defunded. The null object pattern gets culled. Class variables get a hard pass (“weird and wrong”).

Two of the most interesting ideas discussed are “code locality” and “class proliferation”. Many a Ruby veteran has despaired when working on codebases that have replaced spaghetti code with spaghetti classes. Polished Ruby presents persuasive arguments for structural simplicity and against the march of the pattern stampers.

Other spicy snippets include:

  • Dependency injection leads to complexity injection
  • Abstractions are not intrinsically useful
  • Avoid polymorphic associations
  • If you’re not adding things back in, you’re not bleeding enough
  • DRY but at what cost?

Why does Polished Ruby present such a different point of view to conventional community wisdom? It could be because the author, Jeremy Evans, has maintained popular Ruby libraries for over ten years. In an industry where people often change jobs every couple of years, the “develop and dash” approach prevails. Programmers who change codebases every few months are not going to have the same priorities and Evans has not trodden the same path.

Archipelago go go

Polished Ruby is a serious book with an entertaining style. Comedy highlights include a BDD description of the halting problem. Five syllable words are conspicuously avoided with the “island chain approach”. Perl and Python programmers are exclusively referred to as “former Perl/Python programmers”.

A “cursed book” is used to demonstrate class extension using modules and one wonders if the author had a particular cursed book in mind. Similarly, when discussing method design and dealing with a hypothetical “worst data backend you can think of” that “would probably make you pull your hair out”, one suspects that this API is not hypothetical at all.

The Rails framework itself is mostly spared criticism, although ActiveRecord gets a gentle going-over in the database chapter. When the reader is warned to not blindly add “created_at” and “updated_at” to all tables, it is clear which framework is being talked about. Later on, the author alludes to Uncle Bob’s famous criticism of Rails.

The Roda web framework, also maintained by the author, gets a brief mention. I was expecting more about Roda but some of the techniques that make Roda fast are covered in the performance optimisation chapter. Fortunately, many of the sections in the book are based on the 30 (!) presentations linked on the author’s website, https://code.jeremyevans.net, and readers who want to explore topics in more detail will not be disappointed.

My minor quibbles with Polished Ruby are the use of the term “code smell” and the separate suggestion to use the numbered parameters feature, new in Ruby 2.7. In my experience, “code smell” is often used by the over-confident to denigrate the aspirational. Likewise, numbered parameters are clearly evil.

The recommendation to avoid using single-letter variables might raise eyebrows when the author maintains a framework that revolves around the single-letter variable “r”.

Despite its logo’s resemblance to COVID-19, OpenBSD has never gone viral.

The book ends with some OpenBSD evangelism, an operating system famous for its security and unpopularity. When the OpenBSD community talks about “zero-days”, they are referring to the number of days most people have spent using OpenBSD.

As we approach two decades of Rails apps, many monoliths have become too majestic and are starting to look craggy. Several former colleagues have stepped off the Rails train and moved to different languages in search of a better way than the Rails Way. If you are ready to join ActionHolics Anonymous but you want to keep using Ruby, Polished Ruby presents an n+1 step recovery program for ActiveRecord addicts.

Links to purchase the book are available here: https://code.jeremyevans.net/polished-ruby-programming.html

Advertisement

Package and install your own software from a private server using apt and .deb files

This is a complete guide to packaging and installing your own software on Ubuntu and other Debian-based systems using .deb files. By the end, you will be able to:

  • Create simple .deb packages with your own software, installed to the location of your choice
  • Create a (moderately) secure server that makes this software easily available to you (but not others) over the Internet
  • Cryptographically sign your software packages so you don’t have to add [trusted=yes] to your package source files
  • Install and uninstall your software using the Ubuntu package management system (sudo apt install my-custom-package, sudo apt remove my-custom-package)
  • Release updated versions of your packages so you can easily switch between versions

I assume basic Linux usage and familiarity with accessing Linux servers over SSH.

Creating a server

You will need a server and a domain name. I use Linode and domain.com. We will create a $5/month “Nanode” server on Linode running Ubuntu 19.10 and buy a domain for it on domain.com. In this guide:

  • The domain you bought will be my-domain.com
  • The IP address of the Linode server will be 11.22.33.44

Linode is very easy to use and has excellent guides. Create an account and then create a “Nanode” server. You will need to select Ubuntu 19.10 as the Image, choose the Region where the server will be hosted and choose a root password. You can also add in SSH keys if you have one.

Once the Nanode is created, find the IP address in the Linode management page for this server on the right:

linode_ip

Then go to domain.com and buy “my-domain.com”. Once bought, go to the Management page for this domain and click DNS & Nameservers on the left:

Screenshot 2020-03-19 at 00.12.05

Find the Add DNS Record button and click it:

add_dns

Then create an A-name record for your server that looks like this:

add_dns_2

After a short time, you will be able to ping your Linode server using ping packages.my-domain.com and SSH into the server using ssh root@packages.my-domain.com

You can optionally create a custom user, disable root SSH access and enable key-only SSH login to make your server more secure. Many excellent guides are available.

Create your software packages

After logging in to your packaging server, create a simple package. As an example, we will create a package called my-custom-package that installs a simple script in /usr/bin called my-custom-script. We will follow a simplified version of the procedure on linxuconfig.org.

Create a directory called my-custom-package:

mkdir my-custom-package

We will also need to create a DEBIAN directory inside that holds the instructions for the dpkg-deb tool:

mkdir my-custom-package/DEBIAN

And, inside this DEBIAN directory, we will need a file called control that has the dpkg-deb tool’s instructions for making the .deb package. This file needs to contain:

Package: my-custom-package
Version: 1.0
Section: custom
Priority: optional
Architecture: all
Essential: no
Installed-Size: 1024
Maintainer: my-email@my-domain.com
Description: My custom package

Now, we will add the files that will be installed by the package. The system is simple: if you want the package to install a file called my-custom-command into /usr/bin, then create the directories my-custom-package/usr/bin and put a file called my-custom-command into that directory. To create the directories:

mkdir -p my-custom-package/usr/bin

Then create a file called my-custom-package/usr/bin/my-custom-file. In this example, this will be a simple shell script that outputs “It works!”. The file contents should look like this:

#!/bin/bash
echo "It works!"

We also want to make this file executable so that it can be used as a normal command. To do this, run:

chmod +x my-custom-package/usr/bin/my-custom-command

We can now create the package! Run the following command to create the .deb file:

dpkg-deb --build my-custom-package

This will create a file called my-custom-package.deb. Rename it with the version number before doing anything else:

mv my-custom-package.deb my-custom-package-1.0.deb

Creating a “secure” package repository with Apache and Lets Encrypt

We want to easily install and update our software over the Internet, but we don’t want other people to be able to access our packages. To do this, we will:

  • Create an Apache web server on packages.my-domain.com
  • Encrypt access to the web server using Lets Encrypt and HTTPS
  • Add a simple username and password to https://packages.my-domain.com

First, install Apache:

sudo apt install apache2

Then create a directory for hosting the files:

sudo mkdir /var/www/packages.my-domain.com

Remove the default Apache site configuration:

sudo rm /etc/apache2/sites-available/*
sudo rm /etc/apache2/sites-enabled/*

Then create a file called /etc/apache2/sites-available/packages.my-domain.com.conf with the following contents:

<VirtualHost *:80>
ServerAdmin my-email@my-domain.com
ServerName packages.my-domain.com
ServerAlias www.packages.my-domain.com

ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined

DocumentRoot /var/www/packages.my-domain.com

<Directory /var/www/packages.my-domain.com>
  AllowOverride All
</Directory>
AccessFileName .htaccess
</VirtualHost>

Don’t forget to make sure this file ends with .conf or Apache won’t see it!

Enable the site using these commands:

sudo a2ensite packages.my-domain.com
sudo systemctl reload apache2

You should now be able to open packages.my-domain.com in your browser and see this page:

Screenshot 2020-03-19 at 01.17.38

To keep our packages private, we need to add https encryption and a username and password to this site. To add https, install Lets Encrypt’s certbot tool:

sudo apt install python-certbot-apache

Then use the interactive tool to make sure packages.my-domain.com uses https. Run this and select 2 at the end when prompted to redirect using SSL:

sudo certbot --apache -d packages.my-domain.com

Screenshot 2020-03-19 at 01.27.30

You should now see HTTPS enabled in your browser when accessing packages.my-domain.com.

Now that the site is encrypted, we can make access reasonably secure using HTTP Basic authentication. Go to the site directory:

cd /var/www/packages.my-domain.com

Now create a file called .htaccess in /var/www/packages.my-domain.com with the following contents:

AuthName "Login to access packages"
AuthType Basic
AuthUserFile /var/www/packages.my-domain.com/.htpasswd
require user myuser

We also need to create a password. Run the following command and enter mypassword when prompted:

sudo htpasswd -c .htpasswd myuser

Then restart Apache:

sudo systemctl restart apache2

Make sure that the site requires a username and password of myuser and mypassword by opening an incognito window and going to https://packages.my-domain.com. Your browser may cache the page and let you in without a password after Apache is restarted, so an incognito window is a quick way to get around this.

Creating a signed package repository

Ubuntu complains if you don’t use cryptographically signed packages. You can get around this by adding [trusted=yes] when adding your repository to a new computer but we will sign our packages using the guide on ubuntu.com.

This section should be done as the root user:

su root

First, create a signing key using gpg. Follow the prompts and don’t enter a password so we can script later commands for convenience:

 gpg --gen-key

Change to the package directory and export your public key:

cd /var/www/packages.my-domain.com
gpg --output key.gpg --armor --export

This will create a key.gpg file in /var/www/packages.my-domain.com that will start with:

-----BEGIN PGP PUBLIC KEY BLOCK-----

When we install our software on other computers, we will need to add this key to stop apt complaining. That will be explained after the package repository has been set up.

For now, create a directory to store our package:

mkdir /var/www/packages.my-domain.com/all

Then copy my-custom-package-1.0.deb into /var/www/packages.my-domain.com/all.

To sign our package, we will need to install the dpkg-sig tool:

sudo apt install dpkg-sig

To make the package repository creation process easier, we will make a script. Create update_repo.sh in your home directory with the following contents:

#!/bin/bash
cd /var/www/packages.my-domain.com
# Sign each .deb package
find . -type f -name "*.deb" -exec dpkg-sig --sign builder {} \;
apt-ftparchive packages . > Packages
gzip -c Packages > Packages.gz
apt-ftparchive release . > Release
rm -f Release.gpg
gpg -abs -o Release.gpg Release
rm -f InRelease
gpg --clearsign -o InRelease Release

Make this update_repo.sh executable and run it:

chmod +x update_repo.sh
sudo ./update_repo.sh

Congratulations! You now have an operational package repository!

Install your packages on another computer

To test your package repository, we need to add it to another Ubuntu computer. First, we will install the signing key from packages.my-domain.com:

 wget -O - https://myuser:mypassword@packages.my-domain.com/key.gpg | sudo apt-key add -

If you get an error here, make sure the username and password and URL are correct. Note that other people will not be able to access the package site without the username and password.

Next, add the repository to the computer using apt-add-repository:

sudo add-apt-repository 'deb https://packages.my-domain.com /'

We could have added the username and password inline but more recent versions of Ubuntu like to have the login details in a separate file in /etc/apt/auth.conf.d. As root, create the file /etc/apt/auth.conf.d/packages.my-domain.com.conf with the following contents:

machine packages.my-domain.com login myuser password mypassword

We can now update the package list as normal:

sudo apt update

To install our package:

sudo apt install my-custom-package

To check it works, run:

my-custom-command

You can also remove the package as normal using:

sudo apt remove my-custom-package

Releasing an updated version of your package

If we want to release a new version of our package, we can follow the following procedure on our package server. Go back to where you created the package and edit my-custom-package/usr/bin/my-custom-command to look like this:

#!/bin/bash
echo "It works version 1.1!"

Now edit my-custom-package/DEBIAN/control and change the version to 1.1:

Package: my-custom-package
Version: 1.1
Section: custom
Priority: optional
Architecture: all
Essential: no
Installed-Size: 1024
Maintainer: my-email@my-domain.com
Description: My custom package

Create the new package using:

dpkg-deb --build my-custom-package

Then rename and move it to the package repository:

mv my-custom-package.deb /var/www/packages.my-domain.com/all/my-custom-package-1.1.deb

Update the package repository using our script:

sudo update_repo.sh

On the other computers that are using our package repository, we can now see both versions of the package:

sudo apt update

We should see a message that a new version of a package is available:

1 package can be upgraded. Run 'apt list --upgradable' to see it.

We can check the available versions using:

sudo apt-cache policy my-custom-package

This will output:

my-custom-package:
Installed: 1.0
Candidate: 1.1
Version table:
1.1 500
500 https://packages.my-domain.com Packages
*** 1.0 500
500 https://packages.my-domain.com Packages
100 /var/lib/dpkg/status

You can now update your package using:

sudo apt install my-custom-package

Or downgrade it using:

sudo apt install my-custom-package=1.0

Conclusions

Setting up a package repository takes an hour or two, but there are some advantages:

  • You can manage your software using standard tools
  • The extensive Debian/Ubuntu infrastructure for package management is available
  • You have all your versions available for easy downgrades
  • More automated installation means faster setup and fewer mistakes

There are lots of guides online for building more complicated packages, building packages for specific CPU architectures and other ways of hosting packages like sftp and ssh.

Things can get more complicated when the server has more than one GPG key.

I hope this end-to-end guide will help people make use of the packaging system that is built in to Debian, Ubuntu and other derivatives for their own projects and organisations.