<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: John McMillan</title>
    <description>The latest articles on DEV Community by John McMillan (@mcmillanj).</description>
    <link>https://dev.to/mcmillanj</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1262979%2Fd7607d99-a6f7-4c0b-bb33-96b5e757e049.jpg</url>
      <title>DEV Community: John McMillan</title>
      <link>https://dev.to/mcmillanj</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mcmillanj"/>
    <language>en</language>
    <item>
      <title>tfenvy</title>
      <dc:creator>John McMillan</dc:creator>
      <pubDate>Mon, 29 Jan 2024 17:52:40 +0000</pubDate>
      <link>https://dev.to/mcmillanj/tfenvy-3mp</link>
      <guid>https://dev.to/mcmillanj/tfenvy-3mp</guid>
      <description>&lt;p&gt;&lt;strong&gt;Terraform / Git-Bash / Windows&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For almost four years I'd been using a Macbook as my main work machine. One quality of life tool I'd gotten used to was &lt;a href="https://github.com/tfutils/tfenv"&gt;tfenv&lt;/a&gt;, a lovely little tool for managing terraform versions &amp;amp; quickly switching between them. &lt;/p&gt;

&lt;p&gt;I'm now using a Windows machine as my main work laptop having recently started a new job. I'm fairly pragmatic and don't really mind what OS my work laptop runs... at least until I found out that tfenv wasn't available on Windows/git-bash. Sad face. &lt;/p&gt;

&lt;p&gt;I was jealous that I wouldn't be able to use tfenv any more... I wanted something like tfenv for git-bash... tfenvy was born.&lt;/p&gt;

&lt;p&gt;If you're starting to think I'm only writing this article because I'm super proud of the punny name I came up with... you would be right, but still, this might be useful to you if you're using git-bash on Windows to manage terraform code. &lt;/p&gt;

&lt;p&gt;'tfenvy' allows you to&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;List available versions of terraform (stable versions only).&lt;/li&gt;
&lt;li&gt;List locally installed versions of terraform.&lt;/li&gt;
&lt;li&gt;Download a specific version of terraform locally. &lt;/li&gt;
&lt;li&gt;Activate a specific version of terraform to use. &lt;/li&gt;
&lt;li&gt;Update to the latest version of terraform for windows. &lt;/li&gt;
&lt;li&gt;Remove a version of terraform you no longer need. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I don't have an installer yet, but it's simple to setup: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Copy the script below and place it in a suitable PATH for you, making sure it's executable. &lt;/li&gt;
&lt;li&gt;Update TF_DIR and replace YOURUSERNAME with your user name, and make sure you have a 'bin' folder, sorry - directory, there.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/usr/bin/sh

# A super basic attempt to recreate the benefits of choosing a tf version since tfenv isn't yet supported on windows.

# Set Variables:
TF_DOWNLOAD="https://releases.hashicorp.com/terraform/"
TF_DIR="/c/Users/YOURUSERNAME/bin"
NUMARGS=$#



####################################################################################################################
# FUNCTIONS - anything we might need to do more than once - write a function for.

#
# Function to find current ACTIVE version of terraform
#
list_active_tf () {
        CURRENT=`terraform -v 2&amp;gt; /dev/null | head -1 | cut -f 2 -d v`
        if [ -z $CURRENT ] ; then
                echo "TF not currently installed."
                exit 1
        else
                export CURRENT
        fi
}


#
# Function to list ALL available stable releases of terraform
#
list_online_versions () {
        curl -s $TF_DOWNLOAD | grep href | egrep -v "alpha|rc|beta" | sort -n | cut -f 3 -d /
}


#
# Function to find the LATEST stable release of terraform
#
list_latest_version (){
        LATEST=`list_online_versions | tail -1`
        export LATEST
}


#
# Function to compare semantic formatted versions (e.g. 1.2.3)
# Credit to: https://stackoverflow.com/questions/4023830/how-to-compare-two-strings-in-dot-separated-version-format-in-bash
#
vercomp () {
    if [[ $1 == $2 ]]
    then
        return 0
    fi
    local IFS=.
    local i ver1=($1) ver2=($2)
    # fill empty fields in ver1 with zeros
    for ((i=${#ver1[@]}; i&amp;lt;${#ver2[@]}; i++))
    do
        ver1[i]=0
    done
    for ((i=0; i&amp;lt;${#ver1[@]}; i++))
    do
        if [[ -z ${ver2[i]} ]]
        then
            # fill empty fields in ver2 with zeros
            ver2[i]=0
        fi
        if ((10#${ver1[i]} &amp;gt; 10#${ver2[i]}))
        then
            return 1
        fi
        if ((10#${ver1[i]} &amp;lt; 10#${ver2[i]}))
        then
            return 2
        fi
    done
    return 0
}


#
# Function to compare current version with latest available
#
compare_versions () {
        list_latest_version
        list_active_tf
        vercomp $LATEST $CURRENT
        if [ $? == "1" ] ; then
                echo "Update available. Latest=${LATEST}. Current=${CURRENT}."
        else
                echo "Latest version, ${LATEST}, already installed."
        fi
}


#
# Function to list installed versions of terraform
#
list_local_versions () {
        ls $TF_DIR | grep ^terraform-[0-9]*
}


#
# Function to activate the choosen version of terraform
#
activate_tf_version () {
        cp -p ${TF_DIR}/terraform-${WANTED}.exe ${TF_DIR}/terraform.exe
}


#
# Function to download a specific version of terraform
#
get_tf (){
        curl -s ${TF_DOWNLOAD}${WANTED}/terraform_${WANTED}_windows_amd64.zip -o /tmp/terraform_${WANTED}_windows_arm64.zip
        unzip -qp /tmp/terraform_${WANTED}_windows_arm64.zip &amp;gt; ${TF_DIR}/terraform-${WANTED}.exe
}

#
# Function to install the latest version of terraform
#
get_latest_tf () {
        list_latest_version
        WANTED=$LATEST
        get_tf
}


#
# Function to DELETE a specific version of terraform
#
remove_tf (){
        rm -i ${TF_DIR}/terraform-${WANTED}.exe
}


#
# Function to print usage message
#
print_usage () {
        echo "Usage:"
        echo "  -a &amp;lt;version&amp;gt;    Activate terraform &amp;lt;version&amp;gt;."
        echo "          e.g. `basename $0` -a 1.6.6"
        echo ""
        echo "  -d &amp;lt;version&amp;gt;    Download a specific terraform &amp;lt;version&amp;gt;."
        echo "          e.g. `basename $0` -d 1.0.1"
        echo ""
        echo "  -i      List all installed versions of terraform. "
        echo "          e.g. `basename $0` -i"
        echo ""
        echo "  -l      List all stable Terraform versions available from Hashicorp. "
        echo "          e.g. `basename $0` -l"
        echo ""
        echo "  -R &amp;lt;version&amp;gt;    Remove the specified terraform &amp;lt;version&amp;gt;."
        echo "          e.g. `basename $0` -R 1.0.0"
        echo ""
        echo "  -u      Update to latest stable version of terraform."
        echo "          e.g. `basename $0` -u"
        echo ""

}

# End of Functions section. Fun starts now!
####################################################################################################################


# Define Flags
while getopts 'a:ilud:R:' OPTION; do
        case "$OPTION" in
                a)
                        WANTED="$OPTARG"
                        activate_tf_version
                        ;;
                i)
                        list_local_versions
                        ;;
                l)
                        list_online_versions
                        ;;
                u)
                        get_latest_tf
                        activate_tf_version
                        ;;
                d)
                        WANTED="$OPTARG"
                        get_tf
                        ;;
                R)
                        WANTED="$OPTARG"
                        remove_tf
                        ;;
                *)
                        print_usage
                        exit 1
                        ;;
        esac
done
shift "$((OPTIND -1))"

if [ $NUMARGS == 0 ] ; then
        print_usage
        exit
fi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>terraform</category>
      <category>windows</category>
      <category>gitbash</category>
    </item>
    <item>
      <title>Generating dynamic subnets &amp; AZs with Terraform's VPC module</title>
      <dc:creator>John McMillan</dc:creator>
      <pubDate>Mon, 22 Jan 2024 13:14:35 +0000</pubDate>
      <link>https://dev.to/mcmillanj/generating-dynamic-subnets-azs-with-terraforms-vpc-module-3f3k</link>
      <guid>https://dev.to/mcmillanj/generating-dynamic-subnets-azs-with-terraforms-vpc-module-3f3k</guid>
      <description>&lt;p&gt;&lt;strong&gt;The Challenge:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I often use the &lt;a href="https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws/latest"&gt;Terraform VPC module&lt;/a&gt; when creating environments in AWS. And I'm an advocate for writing reusable code where possible... &lt;/p&gt;

&lt;p&gt;So it bugged me that my use of the VPC module relied on a hacky way to cater for setting up subnets in regions with differing numbers of Availability Zones. &lt;/p&gt;

&lt;p&gt;So typically I'd done something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.5.1"

  name            = var.vpc-name
  cidr            = var.vpc-cidr
  azs             = ["${var.region}a", "${var.region}b", "${var.region}c"]
  intra_subnets   = [cidrsubnet(var.vpc-cidr, 8, 0), cidrsubnet(var.vpc-cidr, 8, 1), cidrsubnet(var.vpc-cidr, 8, 2)]
  private_subnets = [cidrsubnet(var.vpc-cidr, 8, 10), cidrsubnet(var.vpc-cidr, 8, 11), cidrsubnet(var.vpc-cidr, 8, 12)]

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, I'd define the &lt;code&gt;region&lt;/code&gt; and &lt;code&gt;vpc-cidr&lt;/code&gt; variables and then when it comes to defining things like &lt;code&gt;intra_subnets&lt;/code&gt; and &lt;code&gt;private_subnets&lt;/code&gt; I'd use the &lt;a href="https://developer.hashicorp.com/terraform/language/functions/cidrsubnet"&gt;cidrsubnet&lt;/a&gt; function to carve up the CIDR into three subnets. &lt;/p&gt;

&lt;p&gt;This approach is fine &lt;em&gt;&lt;strong&gt;if&lt;/strong&gt;&lt;/em&gt; you're only deploying to a region with three AZs. &lt;/p&gt;

&lt;p&gt;But what if you want to deploy to ap-northeast-2 where there are 4 AZs, or us-east-1 where there are 6? &lt;/p&gt;

&lt;p&gt;In those cases you'd need to update the code to add additional entries for each AZ - then consider what shifting up or down you had to do with the &lt;code&gt;netnum&lt;/code&gt; argument in the cidrsubnet function.&lt;/p&gt;

&lt;p&gt;So overall I had functioning code that could be reasonably quickly amended to cater for other regions. But it still wasn't very dynamic and I suspected it could be improved to be so. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Making it better:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Introducing the &lt;a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/availability_zone"&gt;aws_availability_zones&lt;/a&gt; data source, along with the &lt;a href="https://developer.hashicorp.com/terraform/language/functions/index_function"&gt;index&lt;/a&gt; function, and terraform &lt;a href="https://developer.hashicorp.com/terraform/language/values/locals"&gt;locals&lt;/a&gt;, allowed me to modify the code so it caters for differing numbers of availability zones dynamically - meaning no changes to the code are necessary when deploying into different regions. &lt;/p&gt;

&lt;p&gt;The code now looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;data "aws_availability_zones" "my_azs" {
  state = "available"
}

locals {
  azs = data.aws_availability_zones.my_azs.names
  intra_subnets = [
    for az in local.azs : cidrsubnet(var.vpc_cidr, var.intra_subnet_size, index(local.azs, az))
  ]
  private_subnets = [
    for az in local.azs : cidrsubnet(var.vpc_cidr, var.private_subnet_size, (index(local.azs, az) + length(local.azs)))
  ]
}

module "vpc" {
  source               = "terraform-aws-modules/vpc/aws"
  version              = "5.5.1"
  name                 = var.vpc_name
  cidr                 = var.vpc_cidr
  azs                  = local.azs
  intra_subnets        = local.intra_subnets
  private_subnets      = local.private_subnets
}


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is better, and if you just needed to see that example and you understand what's happening, the rest of this blog probably isn't useful to you. &lt;/p&gt;

&lt;p&gt;If you want to understand more about how the code above works, read on. &lt;/p&gt;

&lt;p&gt;Lets look at each section, in turn:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;data "aws_availability_zones" "my_azs" {
  state = "available"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This retrieves a list of all 'available' availability zones in the region you're deploying into. &lt;/p&gt;

&lt;p&gt;This next section defining the locals is a little more complex, but it's not too bad when we break it down:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  azs = data.aws_availability_zones.my_azs.names
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;azs&lt;/code&gt; is straight forward enough, it simply the list of AZ's that our data resource discovered for us.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  intra_subnets = [
    for az in local.azs : cidrsubnet(var.vpc_cidr, var.intra_subnet_size, index(local.azs, az))
  ]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Defining &lt;code&gt;intra_subnets&lt;/code&gt; relies on a 'for' loop.&lt;br&gt;
This loop will iterate over each of the AZ's in our list and use that list to produce a subnet in each AZ regardless of the number of available AZs in the current region. &lt;/p&gt;

&lt;p&gt;As an example, given the following variable definitions: &lt;br&gt;
&lt;code&gt;vpc_cidr&lt;/code&gt; = 10.0.0.0/16 &lt;br&gt;
&lt;code&gt;intra_subnet_size&lt;/code&gt; = 8  (This was introduced as a variable so you can specify different subnet sizes according to your need.)&lt;br&gt;
&lt;code&gt;azs&lt;/code&gt; = [ eu-west-1a, eu-west-1b, eu-west-1c] &lt;/p&gt;

&lt;p&gt;... the first pass of the 'for' loop would expand the cidrsubnet function like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cidrsubnet(10.0.0.0/16, 8, (index(local.azs, eu-west-1a)))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hopefully the first half of the cidrsubnet function should be clear now, you can see that the first subnet will have a /24 netmask (16+8), and that the subnet will be somewhere in the wider 10.0.0.0/16 range. But what position in that range? &lt;/p&gt;

&lt;p&gt;That's what &lt;code&gt;index&lt;/code&gt; is being used for, to determine the value of &lt;code&gt;netnum&lt;/code&gt; for the cidrsubnet function.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;index&lt;/code&gt; function, given a list, and an element in that list, will return the numerical position of that element in that list. e.g. eu-west-1a in the example above is position '0', eu-west-1b is position '1', &amp;amp; eu-west-1c is position '2'. &lt;/p&gt;

&lt;p&gt;Therefore the index portion in the example above evaluates to "0" giving us:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cidrsubnet(10.0.0.0/16, 8, 0)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;... meaning the first intra_subnet, for eu-west-1a, will be defined as 10.0.0.0/24. &lt;/p&gt;

&lt;p&gt;When the loop iterates over eu-west-1b it expands as before, but this time because eu-west-1b is position 1 in the list the 'for' loop will evaulate to&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cidrsubnet(10.0.0.0/16, 8, 1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;... meaning the second intra_subnet, for eu-west-1b, will be defined as 10.0.1.0/24... and so on. &lt;/p&gt;

&lt;p&gt;Moving onto the &lt;code&gt;private_subnets&lt;/code&gt; definition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  private_subnets = [
    for az in local.azs : cidrsubnet(var.vpc_cidr, var.private_subnet_size, (index(local.azs, az) + length(local.azs)))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works on exactly the same premise as before, but we need to prevent them overlapping with the &lt;code&gt;intra_subnets&lt;/code&gt;. This is why the &lt;code&gt;length&lt;/code&gt; function has been added.&lt;/p&gt;

&lt;p&gt;This behaves as before, but the important thing to note here is that &lt;code&gt;length(local.azs)&lt;/code&gt; will return a numerical value, equal to the length of the 'azs' list. &lt;br&gt;
It's used here to add to the index numerical vale to provide an offset equal to the number of AZs. &lt;/p&gt;

&lt;p&gt;Therefore with the same assumptions as before, for eu-west-1, this section will evaluate to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cidrsubnet(10.0.0.0/16, 8, 3)
cidrsubnet(10.0.0.0/16, 8, 4)
cidrsubnet(10.0.0.0/16, 8, 5)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Giving private subnet ranges of:&lt;br&gt;
10.0.3.0/24&lt;br&gt;
10.0.4.0/24&lt;br&gt;
10.0.5.0/24&lt;/p&gt;

&lt;p&gt;Because we use &lt;code&gt;length&lt;/code&gt; to count the number of AZ's in the list it will always offset by the right amount. &lt;/p&gt;

&lt;p&gt;If you wanted to add a third set of subnets, for example &lt;code&gt;public_subnets&lt;/code&gt;, you could use something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  public_subnets = [
    for az in local.azs : cidrsubnet(var.vpc_cidr, var.public_subnet_size, (index(local.azs, az) + length(local.azs)*2))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;i.e. multiply the value of 'length' by 2 to give a third offset value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pulling it together:&lt;/strong&gt;&lt;br&gt;
So having defined the locals you now need to feed those into the VPC module. You can do that with something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module "vpc" {
  source               = "terraform-aws-modules/vpc/aws"
  version              = "5.5.1"
  name                 = var.vpc_name
  cidr                 = var.vpc_cidr
  azs                  = local.azs
  intra_subnets        = local.intra_subnets
  private_subnets      = local.private_subnets
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The beauty of this is that regardless of whether we deploy to eu-west-1 or us-east-1, we've produced code that will deploy the right number of subnets. It will also offset by the correct amount in each region to prevent overlapping ranges. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Further reading:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://developer.hashicorp.com/terraform/language/data-sources"&gt;Terraform Data Sources&lt;/a&gt;&lt;br&gt;
&lt;a href="https://developer.hashicorp.com/terraform/language/values/locals"&gt;Terraform locals&lt;/a&gt;&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>aws</category>
      <category>vpc</category>
    </item>
  </channel>
</rss>
