fbpx

Terraform offers lots of flexibility in coding Infrastructure as Code (IaC) deployments. Using Feature Flags and Environment Toggles can help in increasing the flexibility of the Terraform code to better handle multi-Environment deployments. They can also be used to conditionally deploy certain resources and configurations. These are techniques and design patterns that are greatly helpful to better control what resources are deployed and making multi-Environment maintenance easier to handle.

Let’s get started!

Related: If you’re new to Terraform, or are looking for a bit more information, then we recommend you check out our “Get Started with Terraform on Azure” article. This will give you a great jump start to both using Terraform for infrastructure automation, as well as some tips on using it with Microsoft Azure.

Why use Feature Flags and Environment Toggles?

Feature Flags and Environment Toggles are very common techniques in software development that enable you to implement switches in software so that the code can be released into production but keep certain featured disabled until they are ready to be used. This can help hide features until public availability, or even keep them turned off until they are fully tested and debugged. These techniques allow you to release code to production more quickly and often while maintaining a single source code branch (such as main or master).

Then when the features are ready, you simply turn them on as the code is already out there. This can be done by modifying a configuration file or a build pipeline without requiring the source code to be completely rebuilt. In traditional CI/CD (Continuous Integration / Continuous Deployment) workflows, you can perform a promotion strategy of releasing / deploying the code up through the various environments; such as DEV -> TEST -> STAGE -> PROD. By reusing the same binaries and flipping the feature flags and environment toggles on the releases, the deployment process will incur less risk of file corruption or accidentally deploying untested / unproven code.

The same feature flag and environment toggles can be used in an Infrastructure as Code (IaC) deployment for managing Infrastructure using a tool like Terraform to borrow some of the software development best practices to help build more reliable and flexible infrastructure deployments.

Implement Terraform Feature Flags

Terraform uses the HashiCorp Configuration Language (HCL) as a programming language to declaratively define the configuration of an Infrastructure as Code (IaC) deployment. As with any other program, it has the ability to define variables, loops, and conditions; among other features built into the language. These features enable the ability to build out IaC that can be built to share code with and be flexible when deploying to multiple environments. This is done by implementing feature flags in the code.

Feature flags enable you to flip certain configurations within the Terraform project, or even pass in variables to the project on deployment to customize the IaC being deployed to meet environment or even multi-tenant requirements. Depending on the infrastructure needed, you can use feature flags with the main Terraform project or within Terraform modules used by one or more Terraform projects.

Feature flags in Terraform enable you to conditionally deploy certain resources within the project by configuring individual flags that can be configured differently by environment or even to turn of deploying resources for something that isn’t ready to be deployed yet. This is a great way to build a single Terraform project with a single code base to be used to provisioning multiple environments that may need to be different in some fashion.

Resource Feature Flags

Resource blocks in Terraform are used to define the infrastructure deployment for specific resources based on the specific Terraform resource provider being used. One easy way to define Feature flags to be used is to add them as local variables in the Terraform project. This enables a sort of static feature flag to be configured and then checked into source control with the rest of the Terraform project.

To define the feature flag, the typical approach is to define a single, local boolean variable for the feature the needs to be wrapped in a feature flag. Then you can set the feature flag variable to either true or false to turn the feature on or off in the project. This can be used to easily write the Terraform for infrastructure that’s needed, but keep the feature flag set to false to remain “turned off” until the work is finished or it needs to be deployed when the Terraform project is applied via the terraform apply command.

The local variable for a feature flag can be defined as follows:

locals {
  provision_b59func = true
}

Next, you can use the new feature flag as a condition to determine whether to deploy certain resources defined within the Terraform project. However, Terraform does not contain a sort of provision = local.provision_b59func syntax. Instead, you need to use the count parameter on the Terraform resource. Then with the count parameter, you can use a condition to tell Terraform conditionally provision either 1 or 0 of the declared resource based on the feature flag variable.

Here’s an example of adding the count parameter on a Terraform resource to conditionally declare to provision the resource based on the feature flag.

resource "azurerm_function_app" "b59func" {
  # Conditionally based on feature flag
  count = local.provision_b59func == true ? 1 : 0
  # If 'true', provision 1 of this resource
  # If 'false', don't provision this

  [...]
}

With the count parameter, the condition can be configured to check if the feature flag is true and set the count to 1 for provisioning the resource. Then, also, when the feature flag is set to false the Terraform will be declared to skip provisioning the resource by setting the count to 0 (zero).

Module Feature Flags

When building out a Terraform IaC project, you can break out your Terraform code into modules. This essentially allows you to group multiple Terraform resources into a reusable block of HCL code. You can also think of a Terraform module as a function or method of Terraform code; to borrow terms from software development.

To implement Terraform modules, you will place the Terraform code (.tf) files within a sub-folder or separate folder. Then to reuse the module within you Terraform code, you declare a Terraform module block and use the source argument to tell Terraform where to find the .tf code files that declare the Terraform resources to be deployed for the module.

Here’s an example folder structure for a really simple Terraform project with a module define within the modules/azure-function sub-folder:

Example Terraform Project w/ Module
├── deploy.tf
├── modules
    ├── azure-function
        ├── deploy.tf

In this example, the Terraform project has a single deploy.tf file for simplicity. Most Terraform projects will be comprised of multiple .tf files. Then the modules subfolder is used to contain any additional modules defined within the project. This example shows the modules/azure-function subfolder to define a module for deploying one or more resources as a reusable block of Terraform code.

The modules/azure-function module folder contains the Terraform code for this module. This code will contain all the Terraform code to declare one or more resources this module will be used to deploy and manage. In order to support a feature flag to be passed into the module, the Terraform code will also need to declare a variable to be passed in.

The feature flag value will be passed into the module by declaring an input variable for the module. This will allow the consuming Terraform project to pass in the desired feature flag value to conditionally deploy resources declared within the module.

Here’s a short example for some Terraform code that could be contained within the example module/azure-function module. Notice the code includes a similar condition on the resources based on the feature flag using the count argument, in addition to the variable declaration for the to_provision feature flag of type bool to accept the feature flag.

# Terraform Module: /modules/azure-function/deploy.tf
variable "to_provision" {
  type    = bool
  default = true
}
resource "azurerm_function_app" "b59func" {
  # Conditionally based on feature flag
  count = var.to_provision == true ? 1 : 0
  # If 'true', provision 1 of this resource
  # If 'false', don't provision this

  [...]
}
resource "azurerm_storage_account" "b59storage" {
  # Conditionally based on feature flag
  count = var.to_provision == true ? 1 : 0
  # If 'true', provision 1 of this resource
  # If 'false', don't provision this

  [...]
}

Next, to consume and utilize the module within the main Terraform project, a module block is declared using the source argument to tell Terraform what folder contains the source code for the module being used.

locals {
  provision_b59func = true
}

module "b59func" {
  source = "modules/azure-function"

  to_provision = local.provision_b59func
}

The module block does not use the count argument directly. This may be supported in future versions of Terraform, but at this time count is not supported on modules. As a result, you’ll need to declare an input variable on the module that will be used to pass in the feature flag value with the deployment conditions implemented within the modules code.

Organize Feature Flags within locals

When implementing multiple feature flags within the same Terraform project, it can help to organize them within a group. You can optionally create a local variable that is an object map with individual properties within the object for each feature flag.

Here’s an example of declaring a local.feature_flags variable to contain a couple of feature flags:

locals {
  feature_flags = {
    provision_b59func   = true
    provision_b59iothub = false
  }
}

Grouping feature flag variables under a local object map can help increase clean code practices within the Terraform project by consolidating them all in a single place. It can also help to keep the names of the feature flags from conflicting with the names of other variables declared within the project.

The same method is still used to reference the local object map-based feature flag variables within Terraform code. The only slight difference is that you need to reference them as properties of the “parent” local object.

Here’s an example of referencing the local object map-based feature flags:

resource "azurerm_function_app" "b59func" {
  # Conditionally based on feature flag
  count = local.feature_flags.provision_b59func == true ? 1 : 0
  # If 'true', provision 1 of this resource
  # If 'false', don't provision this

  [...]
}

Implement Terraform Environment Toggles

An Environment Toggle is really just another type of Feature Flag that is used to setup deployment conditions based on the environment being deployed. This allows for the exact same Terraform code to be used to deploy and manage multiple different environments through environment specific switches that are passed in at deployment time.

When writing Terraform code for multiple environments, the simple solution would be to create a new Terraform project for each Environment. This will work just fine at first, however, as the project grows it will become increasingly difficult to manage all the environment projects. This is especially true as the list of deployed environments grows from the initial “Development” and “Test”, to including “Stage”, “Production” and even more environments beyond that.

Having separate Terraform projects for each deployed Environment will require N+1 code bases to be managed as you deploy out all the various environments your project / organization needs. By implementing Environment Toggles, you will create a single code base to manage and simply add conditions within it based on the Environment being deployed to enable certain infrastructure deployment and configurations to be made when deployed.

Pass in Varables to plan and apply commands

The main difference with Environment Toggles is they are passed in to to the Terraform project at the time it’s deployed. More specifically, the Environment Toggle value is passed to the terraform plan and terraform apply commands.

To pass in an Environment Toggle to the terraform plan command you will use the -var argument to pass in the value with the toggle name and value separated by an equal sign in the format of [toggle-name]=[value].

Below is an example of passing in a variable to the terrafrom plan command with the name of environment. This is an easy way to passing a single Environment Toggle / variable to the deployment for the environment being deployed. However, keep in mind that you can also pass in multiple variable to the command using this method as well.

terraform plan \
  -var 'environment=dev' \
  -out deploy.tfplan

Additionally, you can use the exact same -var argument on the terraform apply command to pass in Environment Toggle variables as well:

terraform apply \
  -var 'environment=dev'

Passing in the Environment Toggle variable to the plan and apply commands also require the Terraform project to be configured to use the variable. This can be done by adding a variable block to the Terraform project defining the variable name and value that the Terraform project is to expect to be passed in.

variable "environment" {
  type        = "string"
  description = "The Environment being deployed to"
}

The example above is showing a variable passed in named “environment”. This can be used to pass in a single Environment Toggle variable to tell the Terraform project which environment is being deployed. Keep in mind that you can always define and pass in multiple input variables to pass into the Terraform project in this way. You could keep the environment specific variable and additionally pass in other feature flag variables to implement the custom set of feature flags and environment toggles necessary for your own Terraform solution.

terraform apply \
  -var 'environment=dev' \
  -var 'provision_b59func=true'

Conditionally Provision Based on Environment Toggle

The Terraform input variables pass in to the apply and plan commands can be referenced in the Terraform code in a similar fashion to the local variables. It can be done using the var keyword to base conditions on; like the following:

resource "azurerm_function_app" "b59func" {
  # Conditionally based on feature flag
  count = var.environment == true ? 1 : 0
  # If 'true', provision 1 of this resource
  # If 'false', don't provision this

  [...]
}

This is a great way to write the Terraform code in the project so that it deploys different resources and configures them according to the particular environment being deployed.

Array Values and Conditional Lookup based on Feature Flags

The use of Feature Flags (and Environment Toggles) is not just limited to simple value types. These can also be used to create custom configuration of Terraform resources based on input variables and local variables. This is where a little bit of programmability comes into play within the Terraform project source code.

One particular area where this can be used is with array value types. Some of the properties on Terraform services will accept an array of values. The normal path is hard coding these value arrays, but what if you need to conditionally configure the array custom for the specific environment being deployed?

Creating arrays conditionally based on Environment Toggles or Feature flags can be done easily by creating a local variable with its value set to an Object with property/value pairs with the property name being the Environment Toggle / Feature Flag value that will be used to select which property value to select conditionally. Also, the value of the property will be set to the value needed; such as an array.

Let’s take a look at configuring an Array variable conditionally based on an Environment Toggle!

Define local variable with default value for configuration

To start, we’ll want to create a local variable that contains the default value that will be needed for the Terraform resource property being configured. Here’s a local variable with it’s value set to an Array:

locals {
  # Define Default IP Allow List to apply to all environments
  default_ip_allow_list = {
    "10.50.0.1",
    "10.50.0.2"
  }
}

Create local Variable Object Map of Values to Select

Next, we’ll need to create a local variable with its value set to an Object with property/value pairs that match each of the possible Environment Toggle (or Feature Flag) values that are expected to be used when running the terraform plan and terraform apply commands.

Here’s an example of declaring a local variable with properties for each fo the dev, test, and prod values that will be passed in for the Environment Toggle variable:

locals {
  # Define map of IP Allow List arrays for
  # different Environments you'll be deploying to
  ip_allow_list_environment_map = {
    dev  = local.default_ip_allow_list,
    test = local.default_ip_allow_list,
    prod = local.default_ip_allow_list
  }
}

Notice that each of the property values are set to the same local.default_ip_allow_list variable. This can be used as the base to provide the initial configuration for each Environment set to the default array value needed.

Then you can customize the value for the various environments as necessary for each environment’s requirements. This can be done by directly setting the environment value directly to the value needed; such as Array in this example case. With an Array value, there is another option of using the concat method to take the default value as the base and combine it with another Array of values.

Here’s an example of extending the local variable Object map with an additional Array of values for one of the Environments:

locals {
  # Define map of IP Allow List arrays for
  # different Environments you'll be deploying to
  ip_allow_list_environment_map = {
    dev  = concat(local.default_ip_allow_list, [
      "10.51.0.1"
    ])
    test = local.default_ip_allow_list,
    prod = local.default_ip_allow_list
  }
}

This usage of the concat method to combine the default Array values with another set of values will enable you to create a type of “inheritance” of Array values for a particular environment. The default Array will contain the values that are needed for all environments being deployed, and the concat method will add on the additional values needed for the environment being setup.

Conditionally Lookup Value to Use

Finally, you need to conditionally lookup the particular Object map property value based on the Environment Toggle (or Feature Flag). This can be done using the lookup method that will pick the property value from the Object map variable with the name of the property that matches the Environment Toggle (or Feature Flag) value.

Now that the Object map and default variables have been define, the code needs to be set up to conditionally select the value to use based on the Environment Toggle or Feature Flag being used. This can be done using the lookup method in Terraform to pass in the Object map value and the Environment Toggle value to use to select the required value from the Object map. Also, the last parameter of the method passed in is the default value to use in case the Environment Toggle value isn’t configured within the Object map; basically, the catch all in case it’s not defined.

locals {
  # Conditionally determine which IP Allow List
  # to use for the specified Environment
  ip_allow_list = lookup(
    # object map of values to choose
    local.ip_allow_list_environment_map,
    # Environment Toggle
    var.environment,
    # Default values for edge cases
    local.default_ip_allow_list
  )
}

The above example is using the lookup method to conditionally choose the Object map value based on the Environment Flag passed. It also sets the result of that selection to the value of another local variable that can be used throughout the Terraform project as necessary. Setting the result of this lookup to another local variable allows this variable to be used multiple time within the project. If you only need to use the value once, then you could just set the result of the lookup method call directly to the Terraform resource property that uses it as an alternative.

resource "azurerm_key_vault" "b59vault" {
  [...]

  network_acls {
    [...]
    # Set property based on local variable
    ip_rules = local.ip_allow_list
  }

  [...]
}

Full Array Values and Conditional Lookup Example

Here’s a full Terraform code example of setting up an Array value and conditional lookup based on an Environment Toggle or Feature Flag. Hopefully, this example puts all the pieces together to make setting this all up clearer.

locals {

  # Define Default IP Allow List to apply to all environments
  default_ip_allow_list = [
    "10.50.0.1",
    "10.50.0.2"
  ]

  # Define map of IP Allow List arrays for
  # different Environments you'll be deploying to
  ip_allow_list_environment_map = {

    # Combine Default IP Allow List with additional
    # IP Address to add to the IP Allow List
    # for 'dev' environment
    dev  = concat(local.default_ip_allow_list, [
      "10.51.0.1"
    ])

    test = local.default_ip_allow_list
    prod = local.default_ip_allow_list 
  }

  # Conditionally determine which IP Allow List
  # to use for the specified Environment
  ip_allow_list = lookup(
    local.ip_allow_list_environment_map,
    var.environment,
    local.default_ip_allow_list
  )

}

Wrap Up

Feature Flags and Environment Toggles are a great way to conditionally change the behavior of code at runtime. These are common practices performed in software development and are a great carry over to Infrastructure as Code (IaC) solutions like Terraform. These enable you to conditionally deploy and manage resources based on input variables and local variables within the code and will help simplify your Terraform projects. Using these design patterns will enable you to reduce the amount of Terraform code necessary to build and maintain your multiple deployed environments.

Keep in mind that Terraform Modules are a great way to break out code to be more reusable and can help with simplifying Terraform projects as well. Also, by combining Feature Flags and Environment Toggles with Modules can offer a lot of flexibility for coding multi-Environment deployments using Terraform.

Microsoft MVP

Chris Pietschmann is a Microsoft MVP, HashiCorp Ambassador, and Microsoft Certified Trainer (MCT) with 20+ years of experience designing and building Cloud & Enterprise systems. He has worked with companies of all sizes from startups to large enterprises. He has a passion for technology and sharing what he learns with others to help enable them to learn faster and be more productive.
HashiCorp Ambassador Microsoft Certified Trainer (MCT) Microsoft Certified: Azure Solutions Architect