Sam Merrell's Blog Tinkerer. Parent. ADHD. Developer.

Feed Favorites March 2022

Articles I’ve liked in the past month (5 to be exact).

  • Announcing the HashiCorp Releases API
    • I wish Microsoft would do this for .NET.
  • Your CLI wish is our command 🪄💫
    • Some more details on how the op CLI works. I’m intrigued! I already use the op CLI to pull secrets with 1Password 7 but it is very manual and a bit clumsy. I’m curious how this compares.
  • SSH and Git, meet 1Password 🥰
    • This looks to be very promising! I’ll be a bit honest though, I’m scared to upgrade to 1Password 8 since they’ve moved it to an Electron app. I’m hearing some pretty disappointing performance issues from those who upgraded from the native 1Password 7 macOS app. I’m not as convinced it’s time to move off 1Password, but I’m definitely cautious.

      That said, I’m very interested in the native SSH key support since I keep a few of mine in 1Password already. I’m also curious to see if they improve their GPG key support as well because I find it a bit clumsy how I have to store my GPG keys in 1Password today.

  • Celebrate tiny learning milestones
    • I love the way Julia frames this post. I have a tendency to make big goals for myself and then not complete them and I haven’t thought about the small milestones along the way! I’ve had a goal to learn Swift but haven’t really met that lofty goal. Instead, I did learn about how macOS apps work and how to use interface builder. I built a small pomodoro macOS app using Swift that I was able to compile and use on a different Mac I own. I also learned how to build a SwiftUI app and made a small gauge to use for my pomodoro app. All great milestones I should celebrate!
  • How to get out of the tech debt bottleneck
    • A good introdcution to how a team can run into bottlenecks as they scale up. Many of these resonate with the team I’m on right now. I started right after they got acquired and we’re definitely running into a few of the bottlenecks listed here. I’m curious to see how future articles in this series will be.

Feed Favorites February 2022

Articles I’ve liked in the past month (2 to be exact).

Feed Favorites January 2022

Articles I’ve liked in the past month (2 to be exact).

  • Service Locator is not an Anti-Pattern
    • I appreciate Jimmy’s take on Service Locator. When I was learning about the Service Locator pattern, I was told that it was always A Bad Thing™️. As I’ve grown my skills, I’ve learned to try and avoid blanket statements like that. Similarly, I am not a fan of the term “Best Practice” for similar reasons. There is a time and place for a Best Practice, but it all depends on your situation and the tradeoffs you’re willing to make.
  • How to Adopt a Producer-Consumer Model for HashiCorp Vault

Using Tailscale with an Azure Linux VM and Terraform

I learned about Tailscale from Scott Hanselman’s excellent Podcast Hanselminutes. Since it supports macOS, iOS / iPadOS, Linux, and more I quickly got a simple network created between my devices at home. The setup was completely effortless, and I was able to securely communicate between my devices at home or away. Awesome! I also have a small Azure Subscription that I use to host this website and to play around with Azure itself. How hard would it be to create a small Linux VM in Azure and join it to my Tailscale network?

It turns out Tailscale does have documentation on accessing a Linux VM in Azure, but the steps are all manual. Instead, I wanted to see if I could get an Azure VM created and automatically added to my Tailscale network using Terraform. I already use Terraform to create this site, which is an Azure Static Webapp, so here’s what it took to get things working.

I won’t be showing all the Terraform needed to create the VM, but you can follow the azurerm_linux_virtual_machine docs to create a Linux VM. The first thing I did was create a VNet and Subnet with a Network Security Group. Since I will be using Tailscale to connect to the VM, I want to restrict access into my Subnet.

Here’s the VNet, Subnet, and Network Security Group:

resource "azurerm_virtual_network" "lab" {
  name                = "lab-vnet"
  location            = azurerm_resource_group.lab.location
  resource_group_name =
  address_space       = [""]

resource "azurerm_subnet" "lab" {
  name                 = "lab-subn"
  resource_group_name  =
  virtual_network_name =
  address_prefixes     = [""]

resource "azurerm_network_security_group" "lab_nsg" {
  name                = "lab-nsg"
  location            = azurerm_resource_group.lab.location
  resource_group_name =

The Network Security Group is set up to allow for one TCP inbound connection, for Tailscale, as described in the docs.

resource "azurerm_network_security_rule" "lab_nsg" {
  name                        = "Tailscale"
  description                 = "Tailscale UDP port for direct connections. Reduces latency."
  priority                    = 1010
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "UDP"
  source_port_range           = "*"
  destination_port_range      = 41641
  source_address_prefix       = "*"
  destination_address_prefix  = "*"
  resource_group_name         =
  network_security_group_name =

With the VNet, Subnet, and Network Security Group, I’m ready to create my VM. This is the part that took the most trial and error to work out. I decided that I could use Cloudinit to ensure my VM was configured upon creation, so now I needed to learn the steps it’d take to do that.

resource "azurerm_linux_virtual_machine" "linux01" {
  name                = "linux01"

  # additional properties elided for brevity

  source_image_reference {
    publisher = "Canonical"
    offer     = "0001-com-ubuntu-server-focal"
    sku       = "20_04-lts-gen2"
    version   = "latest"

  custom_data = base64encode(templatefile("${path.module}/tailscale_cloudinit.tpl", {
    tailscale_auth_key = var.tailscale_auth_key

The important part to see here is the custom_data block. You need to base64 encode the text. In addition, I’ve used a template file so that I can pass in an Auth Key for Tailscale. I created a reusable key and then set it as an environment variable as Terraform allows you to set variables as environment variables. My environment variable was set as TFVAR_tailscale_auth_key. You could also use a one-off key if you wanted to create a single VM.

      source: deb focal main
      keyid: 2596A99EAAB33821893C0A79458CA832957F5868
  - tailscale
  - "tailscale up -authkey ${tailscale_auth_key} --advertise-tags=tag:server,tag:lab --advertise-routes=, --accept-dns=false"
  - "echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.conf"
  - "echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.conf"
  - "sysctl -p /etc/sysctl.conf"

While there isn’t seemingly much to this file. It did take quite a bit of troubleshooting to figure this all out. Cloudinit is very nice, but I couldn’t find any way in Azure to see the logs. Instead, while troubleshooting I had to create a public IP to SSH into my VM and review the Cloudinit logs directly.

With the Cloudinit file complete, I was able to run this command and create a new Linux VM that automatically added Tailscale and added itself to my Tailscale network!

$ export TF_VAR_tailscale_auth_key=tskey-kqqJCQ1CNTRL-AAAAAAAAAAAAAAAAAAAAA
$ terraform apply

As you can see below, the new Linux VM is listed in my Tailscale network, and I can reach it from my other devices on the network. All while keeping the Azure Network locked down!

The Tailscale admin interface listing 5 machines including the newly added Linux server from Azure

Let me know what you think on Twitter!

Update 2021.11.26

After posting this, @chrismarget was kind enough to show me the cloudinit_config data source. Once I had some time, I was able to give it a try and it works great! Here’s what the new Terraform code looks like when using cloudinit_config:

First, I need to add the cloudinit provider.

provider "cloudinit" {}

Then I create the cloudinit_config data source with the YAML configuration and a shellscript instead of my runcmd lines I did earlier:

data "cloudinit_config" "cloudinit" {
  base64_encode = true
  gzip          = true
  part {
    content_type = "text/cloud-config"
    content      = file("${path.module}/tailscale/cloudinit.yml")

  part {
    content_type = "text/x-shellscript"
    content = templatefile("${path.module}/tailscale/", {
      tailscale_auth_key = var.tailscale_auth_key

Using this on the virtual machine now looks like:

resource "azurerm_linux_virtual_machine" "linux01" {
  name                = "linux01"

  # additional properties elided for brevity

  source_image_reference {
    publisher = "Canonical"
    offer     = "0001-com-ubuntu-server-focal"
    sku       = "20_04-lts-gen2"
    version   = "latest"

  custom_data = data.cloudinit_config.cloudinit.rendered

What’s great about this method is that it allows me to move the YAML part and shell script portions of my cloudinit file into actual YAML files and shell scripts. This means I can use my regular VS Code tooling to write these scripts before they get packaged into the cloudinit format. So much easier to work with. Thanks for showing me this, Chris!