Splitting your Terraform setup into versioned modules can provide many benefits like DRYer code, decoupling updates to modules from updates to infrastructure and the ability to use different versions of modules for different environments. This all makes working with Terraform a much more enjoyable experience. However it can be tedious updating all the module source references every time you release a new version. So I wanted a quick bash script to do it for me!

#!/usr/local/bin/bash

module_destination="~/terraform-modules"
infrastructure_destination="~/infrastructure"

cd $module_destination

LATEST_TAG=$(git describe --abbrev=0 --tags)

printf "\n\033[37;1mUpdating all modules to Latest Tag: $LATEST_TAG\033[0m\n"

cd $infrastructure_destination

find . -name \*.tf -not -path "*/.terraform/*" -exec sed -i "s/v[0-9]\+.[0-9]\+.[0-9]\+/$LATEST_TAG/" {} +

Note: This script relies on you tagging your module with the format of v0.0.0.

Git Describe

We use git describe to grab the latest tag with this command:

git describe --abbrev=0 --tags

Checking the man pages we learn the following:

git-describe - Describe a commit using the most recent tag reachable from it
--tags
    Instead of using only the annotated tags, use any tag found in refs/tags namespace.
    This option enables matching a lightweight (non-annotated) tag.
--abbrev=<n>
    Instead of using the default 7 hexadecimal digits as the abbreviated object name,
    use <n> digits, or as many digits as needed to form a unique object name.
    An <n> of 0 will suppress long format, only showing the closest tag.

The last line is what is important to us An <n> of 0 will suppress long format, only showing the closest tag. Without this command you'll end with tags like v0.1.156-13-ga481d05 instead of v0.1.156-13.

So the full command of git describe --abbrev=0 --tags simply says

give us the most recent tag minus all the fancy stuff

Find and Sed

This command performs the bulk of our work needed:

find . -name \*.tf -not -path "*/.terraform/*" -exec sed -i --follow-symlinks "s/v[0-9]\+.[0-9]\+.[0-9]\+/$LATEST_TAG/" {} +

Let's break this down piece by piece

find . -name \*.tf

Find all the files that end with .tf

-not -path "*/.terraform/*" 

Ignore files that within folders .terraform

-exec sed

Execute the sed command on the return value of find

sed -i

Change the files in place

"s/v[0-9]\+.[0-9]\+.[0-9]\+/$LATEST_TAG/"

Replace v[0-9]\+.[0-9]\+.[0-9]\+ (which matches our tags like v0.3.2 or v10.12.999) with the that Latest Tag pulled with git

{} +

This is actually part of the -exec flag for find

-exec command {} +
      This variant of the -exec action runs the specified command on the
      selected files, but the command line is built by appending each
      selected file name at the end; the total number of invocations of the
      command will be much less than the number of matched files. The command
      line is built in much the same way that xargs builds its command lines.
      Only one instance of `{}' is allowed within the command...

This is how we can use the result of find like xargs, once for each return value.

And that's it!

Extra Reading

If you want to get more familiar with modules here are some good places to start: