The letter A styled as Alchemists logo. lchemists
Published May 1, 2021 Updated February 17, 2023
Cover
Terraform With Local Plugins

When upgrading from an Apple Intel to Apple Silicon machine, I ran into an issue where some Terraform plugins didn’t support ARM architectures. I’ve found one way of working around this plugin issue is by building and installing a local plugin rather than waiting for the official Terraform registry to be updated. To demonstrate this workaround, I’ll use DNSimple as an example. Specifically, the DNSimple Terraform Plugin. I’ll walk you through how to build this plugin locally so you can work, unencumbered, even though DNSimple is slow to provide ARM 64 architecture support. 😒 For the curious, there is an outstanding issue on this very subject which might be of interest to those wishing to wait for official support.

Setup

I’m not a Go engineer and mine might not be the most elegant solution, but these are the steps I went through to set up the DNSimple Terraform Plugin for local compilation and installation:

cd $HOME/Scratch
brew install go
export GOPATH="$HOME/.cache/go"
git clone https://github.com/dnsimple/terraform-provider-dnsimple.git
cd terraform-provider-dnsimple
make build
cd ..
rm -rf terraform-provider-dnsimple

The above ended up producing the following binary:

$HOME/.cache/go/bin/terraform-provider-dnsimple

With the newly built binary for my ARM 64 architecture in hand, next I needed to inform Terraform where this binary was located.

XDG Cache

I’m a fan of leveraging the XDG specification whenever I can since it keeps my Dotfiles well organized. In addition, I maintain the XDG gem which is a Ruby implementation of the XDG Specification.

Terraform, luckily, provides support for customizing where your configuration is located in addition to where you store your cache and data directories, all of which, fit nicely within an XDG structure by creating the following directory structure:

mkdir -p ~/.cache/terraform/plugins

Then you can add an entry to your dotfiles .bashrc file:

export TF_PLUGIN_CACHE_DIR="$HOME/.cache/terraform/plugins"

Next, I needed to install the previously built DNSimple binary into a directory structure Terraform would recognize. I did this by running the following steps:

mkdir -p $HOME/.cache/terraform/plugins/registry.terraform.io/dnsimple/dnsimple/0.5.1/darwin_arm64
cp $HOME/.cache/go/bin/terraform-provider-dnsimple \
   $HOME/.cache/terraform/plugins/registry.terraform.io/dnsimple/dnsimple/0.5.1/darwin_arm64/terraform-provider-dnsimple_v0.5.1

Local Plugin Structure

Terraform’s local directory structure for the final destination of the binary might be unfamiliar to some -- it definitely was to me. I found that in order run terraform init, terraform plan, etc., Terraform needs a local plugin directory structure that mimics the official Terraform registry. Using Exa, here’s a tree representation of my $HOME/.cache/terraform/plugins root directory:

.
└── registry.terraform.io
   β”œβ”€β”€ dnsimple
   β”‚  └── dnsimple
   β”‚     └── 0.5.1
   β”‚        └── darwin_arm64
   β”‚           └── terraform-provider-dnsimple_v0.5.1
   └── hashicorp
      └── aws
         └── 3.37.0
            └── darwin_arm64
               └── terraform-provider-aws_v3.37.0_x5

The format for this directory structure roughly translates as follows:

<registry>/<hostname>/<namespace>/<version>/<architecture>/<binary>

I don’t believe it’s necessary to suffix your locally built binary with the exact version used in the folder structure above. Instead, I added the suffix to mimic other plugins so it might be possible to remove the suffix entirely if you don’t want that duplication.

Tying Everything Together

Now that we have the DNSimple plugin built for our ARM 64 architecture and the XDG cache configured via our Dotfiles, we can run terraform init. Before we do so, though, I want to point out that it’s a good idea to delete the following from your current Terraform project:

rm -rf .terraform/providers
rm -f .terraform.lock.hcl

Should you be uneasy about this destructive action, feel free to make a backup of the plugins directory and lock file for extra protection. I point this out because I noticed Terraform will not always update the above information if previously generated. Once deleted, now you can run terraform init which will yield the following structure within your .terraform folder:

.terraform
β”œβ”€β”€ modules
β”‚  └── modules.json
β”œβ”€β”€ providers
β”‚  └── registry.terraform.io
β”‚     β”œβ”€β”€ dnsimple
β”‚     β”‚  └── dnsimple
β”‚     β”‚     └── 0.5.1
β”‚     β”‚        └── darwin_arm64 -> /Users/bkuhlmann/.cache/terraform/plugins/registry.terraform.io/dnsimple/dnsimple/0.5.1/darwin_arm64
β”‚     └── hashicorp
β”‚        └── aws
β”‚           └── 3.37.0
β”‚              └── darwin_arm64 -> /Users/bkuhlmann/.cache/terraform/plugins/registry.terraform.io/hashicorp/aws/3.37.0/darwin_arm64
└── terraform.tfstate

Notice: Terraform made symbolic links for both plugins, to the local XDG Cache we configured earlier. I haven’t yet figured out how to teach Terraform to use a hybrid configuration, in which you could have local and remote plugins coexist. For now, Terraform seems to have an all or nothing approach. Even so, with this workaround, you should be able to run your Terraform scripts with your locally built plugin without error. πŸŽ‰