Different Module Versions for Dev and Prod

We’ll cover how to use different versions of a module for the dev and prod environments.

What People Usually Do

First, let’s cover the approach people typically take. Usually, folks have a folder structure that represents the dev and prod environments. This results in a bunch of copy-and-paste. The entire dev and prod structure mirror each other. They then adjust a version number in the respective env folders. Here’s an example structure:

infra
├── dev
│   ├── example.tfvars
│   ├── main.tf
│   └── vars.tf
└── prod
    ├── example.tfvars
    ├── main.tf
    └── vars.tf

The dev/main.tf uses s3 bucket module version 1.15.0

module "bucket" {
  source  = "terraform-aws-modules/s3-bucket/aws"
  version = "1.15.0"
}

The prod/main.tf uses s3 bucket module version 1.14.0

module "bucket" {
  source  = "terraform-aws-modules/s3-bucket/aws"
  version = "1.14.0"
}

While this may seem like a good way to approach it, it introduces a ton of duplication. The rest of the main.tf contents will be duplicated. If you also have variables.tf and outputs.tf, those are also all duplicated.

Different Module Versions with Terraspace

Since Terraspace builds a Terraform project, you can control how to build the project. So you can dynamically set only the version and keep the rest of the code same. This dramatically reduces duplication.

To see how this can be achieved with Terraspace, here’s an example Terraspace project structure:

app
└── stacks
    └── demo
        ├── main.tf
        ├── outputs.tf
        └── variables.tf

The main.tf looks could look like this:

app/stacks/demo/main.tf:

module "bucket" {
  source  = "terraform-aws-modules/s3-bucket/aws"
  version = "1.14" # currently the same version for both dev and prod
}

Generate Helper

We can use helpers to dynamically control the version. Let’s generate a starter helper:

$ terraspace new helper demo --type stack --name version
      create  app/stacks/demo/config/helpers
      create  app/stacks/demo/config/helpers/version_helper.rb
$

The structure now looks like this:

app
└── stacks
    └── demo
        ├── config
        │   └── helpers
        │       └── version_helper.rb
        ├── main.tf
        ├── outputs.tf
        └── variables.tf

Write Custom Helper

We can add a custom version helper with logic that decides which version to use.

app/stacks/bucket/config/helpers/version_helper.rb

module Terraspace::Module::Demo::VersionHelper
  def version
    map = {
      dev:  "1.14",
      prod: "1.15",
    }
    map[Terraspace.env.to_sym] || "1.14"
  end
end

Use Custom Helper

We can now use that helper method in main.tf like so:

app/stacks/demo/main.tf:

module "s3-bucket" {
  source  = "terraform-aws-modules/s3-bucket/aws"
  version = "<%= version %>"
}

Deploy

Now when deployed, dev will use version 1.15

$ TS_ENV=dev  terraspace up demo # use version 1.15
$ cat .terraspace-cache/us-west-2/dev/stacks/demo/main.tf
module "s3-bucket" {
  source  = "terraform-aws-modules/s3-bucket/aws"
  version = "1.15"
}

And prod will use 1.14.

$ TS_ENV=prod terraspace up demo # use version 1.14
$ cat .terraspace-cache/us-west-2/prod/stacks/demo/main.tf
module "s3-bucket" {
  source  = "terraform-aws-modules/s3-bucket/aws"
  version = "1.14"
}
$

Since the Terraform project code is generated by Terraspace, we are able to only make the version different and avoided a lot of duplication.